mirror of
https://github.com/ribru17/ts_query_ls.git
synced 2025-12-23 05:36:52 +00:00
refactor: bump rust to 1.88, use the new niceties (#161)
This commit is contained in:
parent
ac8c28c06f
commit
1d59e4bbd7
10 changed files with 198 additions and 208 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
id: toolchain
|
||||
with:
|
||||
components: clippy
|
||||
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
id: toolchain
|
||||
with:
|
||||
components: rustfmt
|
||||
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
id: toolchain
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
|
|
@ -68,7 +68,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: tree-sitter/setup-action/cli@v1
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
id: toolchain
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
|
|
@ -121,7 +121,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@1.86.0
|
||||
- uses: dtolnay/rust-toolchain@1.88.0
|
||||
id: toolchain
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -50,38 +50,36 @@ pub async fn check_directories(
|
|||
.map(|lang| Arc::new(init_language_data(lang, name)))
|
||||
})
|
||||
});
|
||||
if let Some(lang) = language_data {
|
||||
if let Ok(source) = fs::read_to_string(&path) {
|
||||
return Some(tokio::spawn(async move {
|
||||
if let Some(new_source) = lint_file(
|
||||
&path,
|
||||
&uri,
|
||||
&source,
|
||||
options_arc.clone(),
|
||||
fix,
|
||||
Some(lang),
|
||||
&exit_code,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if fs::write(&path, new_source).is_err() {
|
||||
eprintln!("Failed to write {:?}", path.canonicalize().unwrap());
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
};
|
||||
}));
|
||||
} else {
|
||||
eprintln!("Failed to read {:?}", path.canonicalize().unwrap());
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
} else {
|
||||
let Some(lang) = language_data else {
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
eprintln!(
|
||||
"Could not retrieve language for {:?}",
|
||||
path.canonicalize().unwrap()
|
||||
)
|
||||
);
|
||||
return None;
|
||||
};
|
||||
None
|
||||
let Ok(source) = fs::read_to_string(&path) else {
|
||||
eprintln!("Failed to read {:?}", path.canonicalize().unwrap());
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return None;
|
||||
};
|
||||
Some(tokio::spawn(async move {
|
||||
if let Some(new_source) = lint_file(
|
||||
&path,
|
||||
&uri,
|
||||
&source,
|
||||
options_arc.clone(),
|
||||
fix,
|
||||
Some(lang),
|
||||
&exit_code,
|
||||
)
|
||||
.await
|
||||
&& fs::write(&path, new_source).is_err()
|
||||
{
|
||||
eprintln!("Failed to write {:?}", path.canonicalize().unwrap());
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
};
|
||||
}))
|
||||
});
|
||||
join_all(tasks).await;
|
||||
if format && format_directories(directories, true).await != 0 {
|
||||
|
|
|
|||
|
|
@ -21,31 +21,32 @@ pub async fn format_directories(directories: &[PathBuf], check: bool) -> i32 {
|
|||
let tasks = scm_files.into_iter().map(|path| {
|
||||
let exit_code = exit_code.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Ok(contents) = fs::read_to_string(&path) {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
.set_language(&QUERY_LANGUAGE)
|
||||
.expect("Error loading Query grammar");
|
||||
let tree = parser.parse(contents.as_str(), None).unwrap();
|
||||
let rope = Rope::from(contents.as_str());
|
||||
if let Some(formatted) = formatting::format_document(&rope, &tree.root_node()) {
|
||||
if check {
|
||||
let edits = formatting::diff(&contents, &formatted, &rope);
|
||||
if !edits.is_empty() {
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
eprintln!(
|
||||
"Improper formatting detected for {:?}",
|
||||
path.canonicalize().unwrap()
|
||||
);
|
||||
}
|
||||
} else if fs::write(&path, formatted).is_err() {
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
eprint!("Failed to write to {:?}", path.canonicalize().unwrap())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let Ok(contents) = fs::read_to_string(&path) else {
|
||||
eprintln!("Failed to read {:?}", path.canonicalize().unwrap());
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return;
|
||||
};
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
.set_language(&QUERY_LANGUAGE)
|
||||
.expect("Error loading Query grammar");
|
||||
let tree = parser.parse(contents.as_str(), None).unwrap();
|
||||
let rope = Rope::from(contents.as_str());
|
||||
let Some(formatted) = formatting::format_document(&rope, &tree.root_node()) else {
|
||||
return;
|
||||
};
|
||||
if check {
|
||||
let edits = formatting::diff(&contents, &formatted, &rope);
|
||||
if !edits.is_empty() {
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
eprintln!(
|
||||
"Improper formatting detected for {:?}",
|
||||
path.canonicalize().unwrap()
|
||||
);
|
||||
}
|
||||
} else if fs::write(&path, formatted).is_err() {
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
eprint!("Failed to write to {:?}", path.canonicalize().unwrap())
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -128,11 +128,10 @@ pub async fn lint_directories(directories: &[PathBuf], config: String, fix: bool
|
|||
Some(tokio::spawn(async move {
|
||||
if let Some(new_source) =
|
||||
lint_file(&path, &uri, &source, options, fix, None, &exit_code).await
|
||||
&& fs::write(&path, new_source).is_err()
|
||||
{
|
||||
if fs::write(&path, new_source).is_err() {
|
||||
eprintln!("Failed to write {:?}", path.canonicalize().unwrap());
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
eprintln!("Failed to write {:?}", path.canonicalize().unwrap());
|
||||
exit_code.store(1, std::sync::atomic::Ordering::Relaxed);
|
||||
};
|
||||
}))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -43,57 +43,53 @@ pub async fn profile_directories(directories: &[PathBuf], config: String, per_fi
|
|||
.map(|lang| Arc::new(init_language_data(lang, name)))
|
||||
})
|
||||
});
|
||||
if let Some(lang_data) = language_data {
|
||||
let lang = lang_data.language.clone().unwrap();
|
||||
if let Ok(source) = fs::read_to_string(&path) {
|
||||
Some(tokio::spawn(async move {
|
||||
let mut results = Vec::new();
|
||||
if per_file {
|
||||
let now = Instant::now();
|
||||
let _ = Query::new(&lang, &source);
|
||||
results.push((path_str.clone(), 1, now.elapsed().as_micros()));
|
||||
} else {
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&QUERY_LANGUAGE).unwrap();
|
||||
let tree = parser.parse(&source, None).expect("Tree should exist");
|
||||
let mut cursor = QueryCursor::new();
|
||||
let source_bytes = source.as_bytes();
|
||||
let mut matches = cursor.matches(
|
||||
&PATTERN_DEFINITION_QUERY,
|
||||
tree.root_node(),
|
||||
source_bytes,
|
||||
);
|
||||
while let Some(match_) = matches.next() {
|
||||
for capture in match_.captures {
|
||||
let now = Instant::now();
|
||||
let _ = Query::new(
|
||||
&lang,
|
||||
capture
|
||||
.node
|
||||
.utf8_text(source_bytes)
|
||||
.expect("Source should be UTF-8"),
|
||||
);
|
||||
results.push((
|
||||
path_str.clone(),
|
||||
capture.node.start_position().row + 1,
|
||||
now.elapsed().as_micros(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
}))
|
||||
} else {
|
||||
eprintln!("Failed to read {:?}", path.canonicalize().unwrap());
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let Some(lang_data) = language_data else {
|
||||
eprintln!(
|
||||
"Could not retrieve language for {:?}",
|
||||
path.canonicalize().unwrap()
|
||||
);
|
||||
None
|
||||
}
|
||||
return None;
|
||||
};
|
||||
let lang = lang_data.language.clone().unwrap();
|
||||
let Ok(source) = fs::read_to_string(&path) else {
|
||||
eprintln!("Failed to read {:?}", path.canonicalize().unwrap());
|
||||
return None;
|
||||
};
|
||||
Some(tokio::spawn(async move {
|
||||
if per_file {
|
||||
let now = Instant::now();
|
||||
let _ = Query::new(&lang, &source);
|
||||
return vec![(path_str.clone(), 1, now.elapsed().as_micros())];
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&QUERY_LANGUAGE).unwrap();
|
||||
let tree = parser.parse(&source, None).expect("Tree should exist");
|
||||
let mut cursor = QueryCursor::new();
|
||||
let source_bytes = source.as_bytes();
|
||||
let mut matches =
|
||||
cursor.matches(&PATTERN_DEFINITION_QUERY, tree.root_node(), source_bytes);
|
||||
|
||||
while let Some(match_) = matches.next() {
|
||||
for capture in match_.captures {
|
||||
let now = Instant::now();
|
||||
let _ = Query::new(
|
||||
&lang,
|
||||
capture
|
||||
.node
|
||||
.utf8_text(source_bytes)
|
||||
.expect("Source should be UTF-8"),
|
||||
);
|
||||
results.push((
|
||||
path_str.clone(),
|
||||
capture.node.start_position().row + 1,
|
||||
now.elapsed().as_micros(),
|
||||
));
|
||||
}
|
||||
}
|
||||
results
|
||||
}))
|
||||
});
|
||||
let results = join_all(tasks).await;
|
||||
let mut results = results
|
||||
|
|
|
|||
|
|
@ -159,64 +159,65 @@ pub async fn completion(
|
|||
let in_capture = cursor_after_at_sign || node_is_or_has_ancestor(root, current_node, "capture");
|
||||
let in_predicate = node_is_or_has_ancestor(root, current_node, "predicate");
|
||||
let in_missing = node_is_or_has_ancestor(root, current_node, "missing_node");
|
||||
if !in_capture && !in_predicate {
|
||||
if let Some(language_data) = language_data {
|
||||
let symbols = &language_data.symbols_vec;
|
||||
let supertypes = &language_data.supertype_map;
|
||||
let fields = &language_data.fields_vec;
|
||||
let in_anon = node_is_or_has_ancestor(root, current_node, "string") && !in_predicate;
|
||||
let top_level = current_node.kind() == "program";
|
||||
let in_negated_field = current_node.kind() == "negated_field"
|
||||
|| cursor_after_exclamation_point
|
||||
|| (current_node.kind() == "identifier"
|
||||
&& current_node
|
||||
.parent()
|
||||
.is_some_and(|p| p.kind() == "negated_field"));
|
||||
if !in_capture
|
||||
&& !in_predicate
|
||||
&& let Some(language_data) = language_data
|
||||
{
|
||||
let symbols = &language_data.symbols_vec;
|
||||
let supertypes = &language_data.supertype_map;
|
||||
let fields = &language_data.fields_vec;
|
||||
let in_anon = node_is_or_has_ancestor(root, current_node, "string") && !in_predicate;
|
||||
let top_level = current_node.kind() == "program";
|
||||
let in_negated_field = current_node.kind() == "negated_field"
|
||||
|| cursor_after_exclamation_point
|
||||
|| (current_node.kind() == "identifier"
|
||||
&& current_node
|
||||
.parent()
|
||||
.is_some_and(|p| p.kind() == "negated_field"));
|
||||
|
||||
if in_negated_field {
|
||||
for field in fields.clone() {
|
||||
completion_items.push(CompletionItem {
|
||||
label: field,
|
||||
kind: Some(CompletionItemKind::FIELD),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
return Ok(Some(CompletionResponse::Array(completion_items)));
|
||||
if in_negated_field {
|
||||
for field in fields.clone() {
|
||||
completion_items.push(CompletionItem {
|
||||
label: field,
|
||||
kind: Some(CompletionItemKind::FIELD),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if !top_level {
|
||||
for symbol in symbols.iter() {
|
||||
if (in_anon && !symbol.named) || (!in_anon && symbol.named) {
|
||||
completion_items.push(CompletionItem {
|
||||
label: symbol.label.clone(),
|
||||
kind: if symbol.named {
|
||||
if !supertypes.contains_key(symbol) {
|
||||
Some(CompletionItemKind::CLASS)
|
||||
} else {
|
||||
Some(CompletionItemKind::INTERFACE)
|
||||
}
|
||||
return Ok(Some(CompletionResponse::Array(completion_items)));
|
||||
}
|
||||
if !top_level {
|
||||
for symbol in symbols.iter() {
|
||||
if (in_anon && !symbol.named) || (!in_anon && symbol.named) {
|
||||
completion_items.push(CompletionItem {
|
||||
label: symbol.label.clone(),
|
||||
kind: if symbol.named {
|
||||
if !supertypes.contains_key(symbol) {
|
||||
Some(CompletionItemKind::CLASS)
|
||||
} else {
|
||||
Some(CompletionItemKind::CONSTANT)
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
Some(CompletionItemKind::INTERFACE)
|
||||
}
|
||||
} else {
|
||||
Some(CompletionItemKind::CONSTANT)
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
if !in_missing && !in_anon {
|
||||
if !top_level {
|
||||
completion_items.push(CompletionItem {
|
||||
label: String::from("MISSING"),
|
||||
kind: Some(CompletionItemKind::KEYWORD),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
for field in fields {
|
||||
completion_items.push(CompletionItem {
|
||||
label: format!("{field}: "),
|
||||
kind: Some(CompletionItemKind::FIELD),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
if !in_missing && !in_anon {
|
||||
if !top_level {
|
||||
completion_items.push(CompletionItem {
|
||||
label: String::from("MISSING"),
|
||||
kind: Some(CompletionItemKind::KEYWORD),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
for field in fields {
|
||||
completion_items.push(CompletionItem {
|
||||
label: format!("{field}: "),
|
||||
kind: Some(CompletionItemKind::FIELD),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,30 +130,30 @@ pub async fn populate_import_documents(
|
|||
imported_uris: &Vec<(u32, u32, Option<Url>)>,
|
||||
) {
|
||||
for (_, _, uri) in imported_uris {
|
||||
if let Some(uri) = uri {
|
||||
if !backend.document_map.contains_key(uri) {
|
||||
let path = uri.to_file_path().unwrap();
|
||||
if let Ok(contents) = fs::read_to_string(path) {
|
||||
let rope = Rope::from_str(&contents);
|
||||
let mut parser = Parser::new();
|
||||
parser
|
||||
.set_language(&QUERY_LANGUAGE)
|
||||
.expect("Error loading Query grammar");
|
||||
let tree = parser.parse(&contents, None).unwrap();
|
||||
let nested_imported_uris = get_imported_uris(backend, uri, &rope, &tree).await;
|
||||
backend.document_map.insert(
|
||||
uri.clone(),
|
||||
DocumentData {
|
||||
rope,
|
||||
tree,
|
||||
language_name: None,
|
||||
version: -1,
|
||||
imported_uris: nested_imported_uris.clone(),
|
||||
},
|
||||
);
|
||||
Box::pin(populate_import_documents(backend, &nested_imported_uris)).await
|
||||
}
|
||||
}
|
||||
if let Some(uri) = uri
|
||||
&& !backend.document_map.contains_key(uri)
|
||||
&& let Ok(contents) = uri
|
||||
.to_file_path()
|
||||
.and_then(|path| fs::read_to_string(path).map_err(|_| ()))
|
||||
{
|
||||
let rope = Rope::from_str(&contents);
|
||||
let mut parser = Parser::new();
|
||||
parser
|
||||
.set_language(&QUERY_LANGUAGE)
|
||||
.expect("Error loading Query grammar");
|
||||
let tree = parser.parse(&contents, None).unwrap();
|
||||
let nested_imported_uris = get_imported_uris(backend, uri, &rope, &tree).await;
|
||||
backend.document_map.insert(
|
||||
uri.clone(),
|
||||
DocumentData {
|
||||
rope,
|
||||
tree,
|
||||
language_name: None,
|
||||
version: -1,
|
||||
imported_uris: nested_imported_uris.clone(),
|
||||
},
|
||||
);
|
||||
Box::pin(populate_import_documents(backend, &nested_imported_uris)).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ pub async fn document_symbol(
|
|||
let capture_node = capture.node;
|
||||
let node_text = capture_node.text(rope);
|
||||
let parent = capture_node.parent().unwrap();
|
||||
#[allow(deprecated)]
|
||||
document_symbols.push(DocumentSymbol {
|
||||
name: node_text,
|
||||
kind: SymbolKind::VARIABLE,
|
||||
|
|
@ -43,6 +42,7 @@ pub async fn document_symbol(
|
|||
// TODO: Structure this hierarchically
|
||||
children: None,
|
||||
tags: None,
|
||||
#[allow(deprecated)]
|
||||
deprecated: None,
|
||||
});
|
||||
}
|
||||
|
|
@ -172,7 +172,6 @@ mod test {
|
|||
|
||||
// Assert
|
||||
let actual = Some(DocumentSymbolResponse::Nested(
|
||||
#[allow(deprecated)]
|
||||
symbols
|
||||
.iter()
|
||||
.map(|s| DocumentSymbol {
|
||||
|
|
@ -183,6 +182,7 @@ mod test {
|
|||
detail: None,
|
||||
children: None,
|
||||
tags: None,
|
||||
#[allow(deprecated)]
|
||||
deprecated: None,
|
||||
})
|
||||
.collect(),
|
||||
|
|
|
|||
|
|
@ -230,8 +230,7 @@ fn format_iter<'a>(
|
|||
}
|
||||
}
|
||||
if map.comment_fix.contains(id) {
|
||||
let text = child.text(rope);
|
||||
if let Some(mat) = COMMENT_PAT.captures(text.as_str()) {
|
||||
if let Some(mat) = COMMENT_PAT.captures(&child.text(rope)) {
|
||||
lines
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
|
|
@ -310,10 +309,10 @@ fn handle_predicate(
|
|||
Some(node) => node.kind(),
|
||||
};
|
||||
for arg in &args[1..] {
|
||||
if let QueryPredicateArg::String(kind) = arg {
|
||||
if node_type == kind.deref() {
|
||||
return false;
|
||||
}
|
||||
if let QueryPredicateArg::String(kind) = arg
|
||||
&& node_type == kind.deref()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
src/util.rs
30
src/util.rs
|
|
@ -285,13 +285,12 @@ fn get_first_valid_file_config(workspace_uris: Vec<Url>) -> Option<Options> {
|
|||
if let Ok(mut path) = folder_url.to_file_path() {
|
||||
let mut config_path = path.join(".tsqueryrc.json");
|
||||
loop {
|
||||
if config_path.is_file() {
|
||||
let data = fs::read_to_string(&config_path)
|
||||
if config_path.is_file()
|
||||
&& let Some(options) = fs::read_to_string(&config_path)
|
||||
.ok()
|
||||
.and_then(|data| serde_json::from_str(&data).ok());
|
||||
if let Some(options) = data {
|
||||
return options;
|
||||
}
|
||||
.and_then(|data| serde_json::from_str(&data).ok())
|
||||
{
|
||||
return options;
|
||||
}
|
||||
path = match path.parent() {
|
||||
Some(parent) => parent.into(),
|
||||
|
|
@ -386,23 +385,20 @@ pub async fn get_file_uris(backend: &Backend, language_name: &str, query_type: &
|
|||
.iter()
|
||||
.filter_map(|uri| uri.to_file_path().ok())
|
||||
.collect::<Vec<_>>();
|
||||
let scm_files = get_scm_files(&dirs);
|
||||
let language_retrieval_regexes = &backend.options.read().await.language_retrieval_patterns;
|
||||
|
||||
let mut urls = Vec::new();
|
||||
|
||||
for scm_file in scm_files {
|
||||
for scm_file in get_scm_files(&dirs) {
|
||||
for re in language_retrieval_regexes {
|
||||
if let Some(lang_name) = re
|
||||
.captures(&scm_file.canonicalize().unwrap().to_string_lossy())
|
||||
.and_then(|caps| caps.get(1))
|
||||
if scm_file.file_stem().is_some_and(|stem| stem == query_type)
|
||||
&& let Some(lang_name) = re
|
||||
.captures(&scm_file.canonicalize().unwrap().to_string_lossy())
|
||||
.and_then(|caps| caps.get(1))
|
||||
&& lang_name.as_str() == language_name
|
||||
{
|
||||
if lang_name.as_str() == language_name
|
||||
&& scm_file.file_stem().is_some_and(|stem| stem == query_type)
|
||||
{
|
||||
urls.push(Url::from_file_path(&scm_file).unwrap());
|
||||
break;
|
||||
}
|
||||
urls.push(Url::from_file_path(&scm_file).unwrap());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue