From fcead6739a6a95fd3c96c2ea48f9ab27c3008aef Mon Sep 17 00:00:00 2001 From: nkxxll Date: Fri, 12 Dec 2025 17:45:31 +0100 Subject: [PATCH 1/6] add ignore current file to code actions --- crates/codebook-lsp/src/lsp.rs | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/crates/codebook-lsp/src/lsp.rs b/crates/codebook-lsp/src/lsp.rs index 13c2ebe..59293dd 100644 --- a/crates/codebook-lsp/src/lsp.rs +++ b/crates/codebook-lsp/src/lsp.rs @@ -38,6 +38,7 @@ pub struct Backend { enum CodebookCommand { AddWord, AddWordGlobal, + IgnoreFile, Unknown, } @@ -46,6 +47,7 @@ impl From<&str> for CodebookCommand { match command { "codebook.addWord" => CodebookCommand::AddWord, "codebook.addWordGlobal" => CodebookCommand::AddWordGlobal, + "codebook.ignoreFile" => CodebookCommand::IgnoreFile, _ => CodebookCommand::Unknown, } } @@ -56,6 +58,7 @@ impl From for String { match command { CodebookCommand::AddWord => "codebook.addWord".to_string(), CodebookCommand::AddWordGlobal => "codebook.addWordGlobal".to_string(), + CodebookCommand::IgnoreFile => "codebook.ignoreFile".to_string(), CodebookCommand::Unknown => "codebook.unknown".to_string(), } } @@ -93,6 +96,7 @@ impl LanguageServer for Backend { commands: vec![ CodebookCommand::AddWord.into(), CodebookCommand::AddWordGlobal.into(), + CodebookCommand::IgnoreFile.into(), ], work_done_progress_options: Default::default(), }), @@ -256,6 +260,21 @@ impl LanguageServer for Backend { data: None, })); } + let relative_path = self.get_relative_path(¶ms.text_document.uri); + actions.push(CodeActionOrCommand::CodeAction(CodeAction { + title: format!("Add current file to ignore list"), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: None, + edit: None, + command: Some(Command { + title: format!("Add current file to ignore list"), + command: CodebookCommand::IgnoreFile.into(), + arguments: Some(vec![relative_path.into()]), + }), + is_preferred: None, + disabled: None, + data: None, + })); match actions.is_empty() { true => Ok(None), false => Ok(Some(actions)), @@ -294,6 +313,24 @@ impl LanguageServer for Backend { } Ok(None) } + CodebookCommand::IgnoreFile => { + let config = self.config_handle(); + let file_uri = params + .arguments + .first() + .expect("CodebookCommand::IgnoreFile: There has to be a file URI here!"); + let updated = self.add_ignore_file( + config.as_ref(), + file_uri.as_str().expect( + "CodebookCommand::IgnoreFile: Argument should be convertable to a String!", + ), + ); + if updated { + let _ = config.save(); + self.recheck_all().await; + } + Ok(None) + } CodebookCommand::Unknown => Ok(None), } } @@ -382,6 +419,7 @@ impl Backend { } should_save } + fn add_words_global( &self, config: &CodebookConfigFile, @@ -404,6 +442,36 @@ impl Backend { should_save } + fn get_relative_path(&self, uri: &Url) -> String { + let file_path = uri.to_file_path().unwrap_or_default(); + let absolute_workspace_dir = &self.workspace_dir.canonicalize(); + + match absolute_workspace_dir { + Ok(dir) => match file_path.strip_prefix(dir) { + Ok(relative) => relative.to_string_lossy().to_string(), + Err(_) => file_path.to_string_lossy().to_string(), + }, + Err(err) => { + info!("Could not get absolute path from workspace directory. Error: {err}."); + file_path.to_string_lossy().to_string() + } + } + } + + fn add_ignore_file(&self, config: &CodebookConfigFile, file_uri: &str) -> bool { + match config.add_ignore(file_uri) { + Ok(true) => true, + Ok(false) => { + info!("File {file_uri} already exists in the ignored files."); + false + } + Err(e) => { + error!("Failed to add ignore file: {e}"); + false + } + } + } + fn make_suggestion(&self, suggestion: &str, range: &Range, uri: &Url) -> CodeAction { let title = format!("Replace with '{suggestion}'"); let mut map = HashMap::new(); From b1bcd8f8ae7ae9560a0e2b9ea6bfd530e24af73d Mon Sep 17 00:00:00 2001 From: nkxxll Date: Sun, 14 Dec 2025 01:41:52 +0100 Subject: [PATCH 2/6] compute relative path only if the action is called --- crates/codebook-lsp/src/lsp.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/codebook-lsp/src/lsp.rs b/crates/codebook-lsp/src/lsp.rs index 59293dd..ea39b6b 100644 --- a/crates/codebook-lsp/src/lsp.rs +++ b/crates/codebook-lsp/src/lsp.rs @@ -260,7 +260,6 @@ impl LanguageServer for Backend { data: None, })); } - let relative_path = self.get_relative_path(¶ms.text_document.uri); actions.push(CodeActionOrCommand::CodeAction(CodeAction { title: format!("Add current file to ignore list"), kind: Some(CodeActionKind::QUICKFIX), @@ -269,7 +268,7 @@ impl LanguageServer for Backend { command: Some(Command { title: format!("Add current file to ignore list"), command: CodebookCommand::IgnoreFile.into(), - arguments: Some(vec![relative_path.into()]), + arguments: Some(vec![params.text_document.uri.to_string().into()]), }), is_preferred: None, disabled: None, @@ -318,13 +317,12 @@ impl LanguageServer for Backend { let file_uri = params .arguments .first() - .expect("CodebookCommand::IgnoreFile: There has to be a file URI here!"); - let updated = self.add_ignore_file( - config.as_ref(), - file_uri.as_str().expect( - "CodebookCommand::IgnoreFile: Argument should be convertable to a String!", - ), - ); + .expect("CodebookCommand::IgnoreFile: There has to be a file URI here!") + .as_str() + .expect( + "CodebookCommand::IgnoreFile: Argument should be convertible to a String.", + ); + let updated = self.add_ignore_file(config.as_ref(), file_uri); if updated { let _ = config.save(); self.recheck_all().await; @@ -442,7 +440,9 @@ impl Backend { should_save } - fn get_relative_path(&self, uri: &Url) -> String { + fn get_relative_path(&self, uri: &str) -> String { + let uri = Url::parse(uri) + .expect("This is a correctly formatted URL because it comes from the LSP protocol."); let file_path = uri.to_file_path().unwrap_or_default(); let absolute_workspace_dir = &self.workspace_dir.canonicalize(); @@ -459,7 +459,8 @@ impl Backend { } fn add_ignore_file(&self, config: &CodebookConfigFile, file_uri: &str) -> bool { - match config.add_ignore(file_uri) { + let relative_path = &self.get_relative_path(file_uri); + match config.add_ignore(relative_path) { Ok(true) => true, Ok(false) => { info!("File {file_uri} already exists in the ignored files."); From 739673cec616773b9948779253e54e39557123fa Mon Sep 17 00:00:00 2001 From: nkxxll Date: Sun, 14 Dec 2025 01:41:52 +0100 Subject: [PATCH 3/6] canonicalize both paths to ensure match if there is one --- crates/codebook-lsp/src/lsp.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/codebook-lsp/src/lsp.rs b/crates/codebook-lsp/src/lsp.rs index ea39b6b..efbb179 100644 --- a/crates/codebook-lsp/src/lsp.rs +++ b/crates/codebook-lsp/src/lsp.rs @@ -447,8 +447,11 @@ impl Backend { let absolute_workspace_dir = &self.workspace_dir.canonicalize(); match absolute_workspace_dir { - Ok(dir) => match file_path.strip_prefix(dir) { - Ok(relative) => relative.to_string_lossy().to_string(), + Ok(dir) => match file_path.canonicalize() { + Ok(canon_file_path) => match canon_file_path.strip_prefix(dir) { + Ok(relative) => relative.to_string_lossy().to_string(), + Err(_) => file_path.to_string_lossy().to_string(), + }, Err(_) => file_path.to_string_lossy().to_string(), }, Err(err) => { From 7a04410da4b5f6f1bd515e16ee45a69ffdf9349b Mon Sep 17 00:00:00 2001 From: nkxxll <84667783+nkxxll@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:37:01 +0100 Subject: [PATCH 4/6] use `.to_string()` instead of `format!(...)` --- crates/codebook-lsp/src/lsp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/codebook-lsp/src/lsp.rs b/crates/codebook-lsp/src/lsp.rs index efbb179..14881f7 100644 --- a/crates/codebook-lsp/src/lsp.rs +++ b/crates/codebook-lsp/src/lsp.rs @@ -261,12 +261,12 @@ impl LanguageServer for Backend { })); } actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: format!("Add current file to ignore list"), + title: "Add current file to ignore list".to_string(), kind: Some(CodeActionKind::QUICKFIX), diagnostics: None, edit: None, command: Some(Command { - title: format!("Add current file to ignore list"), + title: "Add current file to ignore list".to_string(), command: CodebookCommand::IgnoreFile.into(), arguments: Some(vec![params.text_document.uri.to_string().into()]), }), From 4387e67d235adc0b4f68dc671e5f9c5f516c8e0a Mon Sep 17 00:00:00 2001 From: Bo Lopker Date: Tue, 23 Dec 2025 12:54:14 -0800 Subject: [PATCH 5/6] Fix relative path detection --- .claude/settings.local.json | 8 + codebook.toml | 2 +- crates/codebook-lsp/src/lsp.rs | 164 +++++++++++++----- .../src/dictionaries/combined.gen.txt | 1 + 4 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..3a26949 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(cargo test:*)", + "Bash(cargo clippy:*)" + ] + } +} diff --git a/codebook.toml b/codebook.toml index d171fdc..63bc351 100644 --- a/codebook.toml +++ b/codebook.toml @@ -23,8 +23,8 @@ flag_words = [ "fixme", ] ignore_paths = [ - "target/**/*", "**/*.json", ".git/**/*", + "target/**/*", ] ignore_patterns = ['\b[ATCG]+\b'] diff --git a/crates/codebook-lsp/src/lsp.rs b/crates/codebook-lsp/src/lsp.rs index 14881f7..4ed4adb 100644 --- a/crates/codebook-lsp/src/lsp.rs +++ b/crates/codebook-lsp/src/lsp.rs @@ -26,6 +26,26 @@ use crate::lsp_logger; const SOURCE_NAME: &str = "Codebook"; +/// Computes the relative path of a file from a workspace directory. +/// Returns the relative path if the file is within the workspace, otherwise returns the absolute path. +fn compute_relative_path(workspace_dir: &Path, file_path: &Path) -> String { + let absolute_workspace_dir = workspace_dir.canonicalize(); + + match absolute_workspace_dir { + Ok(dir) => match file_path.canonicalize() { + Ok(canon_file_path) => match canon_file_path.strip_prefix(&dir) { + Ok(relative) => relative.to_string_lossy().to_string(), + Err(_) => file_path.to_string_lossy().to_string(), + }, + Err(_) => file_path.to_string_lossy().to_string(), + }, + Err(err) => { + info!("Could not get absolute path from workspace directory. Error: {err}."); + file_path.to_string_lossy().to_string() + } + } +} + pub struct Backend { client: Client, workspace_dir: PathBuf, @@ -186,11 +206,13 @@ impl LanguageServer for Backend { None => return Ok(None), }; + let mut has_codebook_diagnostic = false; for diag in params.context.diagnostics { // Only process our own diagnostics if diag.source.as_deref() != Some(SOURCE_NAME) { continue; } + has_codebook_diagnostic = true; let line = doc .text .lines() @@ -260,20 +282,22 @@ impl LanguageServer for Backend { data: None, })); } - actions.push(CodeActionOrCommand::CodeAction(CodeAction { - title: "Add current file to ignore list".to_string(), - kind: Some(CodeActionKind::QUICKFIX), - diagnostics: None, - edit: None, - command: Some(Command { + if has_codebook_diagnostic { + actions.push(CodeActionOrCommand::CodeAction(CodeAction { title: "Add current file to ignore list".to_string(), - command: CodebookCommand::IgnoreFile.into(), - arguments: Some(vec![params.text_document.uri.to_string().into()]), - }), - is_preferred: None, - disabled: None, - data: None, - })); + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: None, + edit: None, + command: Some(Command { + title: "Add current file to ignore list".to_string(), + command: CodebookCommand::IgnoreFile.into(), + arguments: Some(vec![params.text_document.uri.to_string().into()]), + }), + is_preferred: None, + disabled: None, + data: None, + })); + } match actions.is_empty() { true => Ok(None), false => Ok(Some(actions)), @@ -313,15 +337,15 @@ impl LanguageServer for Backend { Ok(None) } CodebookCommand::IgnoreFile => { - let config = self.config_handle(); - let file_uri = params + let Some(file_uri) = params .arguments .first() - .expect("CodebookCommand::IgnoreFile: There has to be a file URI here!") - .as_str() - .expect( - "CodebookCommand::IgnoreFile: Argument should be convertible to a String.", - ); + .and_then(|arg| arg.as_str()) + else { + error!("IgnoreFile command missing or invalid file URI argument"); + return Ok(None); + }; + let config = self.config_handle(); let updated = self.add_ignore_file(config.as_ref(), file_uri); if updated { let _ = config.save(); @@ -440,30 +464,23 @@ impl Backend { should_save } - fn get_relative_path(&self, uri: &str) -> String { - let uri = Url::parse(uri) - .expect("This is a correctly formatted URL because it comes from the LSP protocol."); - let file_path = uri.to_file_path().unwrap_or_default(); - let absolute_workspace_dir = &self.workspace_dir.canonicalize(); - - match absolute_workspace_dir { - Ok(dir) => match file_path.canonicalize() { - Ok(canon_file_path) => match canon_file_path.strip_prefix(dir) { - Ok(relative) => relative.to_string_lossy().to_string(), - Err(_) => file_path.to_string_lossy().to_string(), - }, - Err(_) => file_path.to_string_lossy().to_string(), - }, - Err(err) => { - info!("Could not get absolute path from workspace directory. Error: {err}."); - file_path.to_string_lossy().to_string() + fn get_relative_path(&self, uri: &str) -> Option { + let parsed_uri = match Url::parse(uri) { + Ok(u) => u, + Err(e) => { + error!("Failed to parse URI '{uri}': {e}"); + return None; } - } + }; + let file_path = parsed_uri.to_file_path().unwrap_or_default(); + Some(compute_relative_path(&self.workspace_dir, &file_path)) } fn add_ignore_file(&self, config: &CodebookConfigFile, file_uri: &str) -> bool { - let relative_path = &self.get_relative_path(file_uri); - match config.add_ignore(relative_path) { + let Some(relative_path) = self.get_relative_path(file_uri) else { + return false; + }; + match config.add_ignore(&relative_path) { Ok(true) => true, Ok(false) => { info!("File {file_uri} already exists in the ignored files."); @@ -540,6 +557,9 @@ impl Backend { let file_path = doc.uri.to_file_path().unwrap_or_default(); debug!("Spell-checking file: {file_path:?}"); + // Compute relative path for ignore pattern matching + let relative_path = compute_relative_path(&self.workspace_dir, &file_path); + // Convert utf8 byte offsets to utf16 let offsets = StringOffsets::::new(&doc.text); @@ -548,9 +568,8 @@ impl Backend { let lang_type = lang.and_then(|lang| LanguageType::from_str(lang).ok()); debug!("Document identified as type {lang_type:?} from {lang:?}"); let cb = self.codebook_handle(); - let fp = file_path.clone(); let spell_results = task::spawn_blocking(move || { - cb.spell_check(&doc.text, lang_type, Some(fp.to_str().unwrap_or_default())) + cb.spell_check(&doc.text, lang_type, Some(&relative_path)) }) .await; @@ -586,3 +605,64 @@ impl Backend { // debug!("Published diagnostics for: {:?}", file_path); } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + #[test] + fn test_compute_relative_path_within_workspace() { + let workspace = tempdir().unwrap(); + let workspace_path = workspace.path(); + + // Create a file inside the workspace + let subdir = workspace_path.join("src"); + fs::create_dir_all(&subdir).unwrap(); + let file_path = subdir.join("test.rs"); + fs::write(&file_path, "test").unwrap(); + + let result = compute_relative_path(workspace_path, &file_path); + assert_eq!(result, "src/test.rs"); + } + + #[test] + fn test_compute_relative_path_outside_workspace() { + let workspace = tempdir().unwrap(); + let other_dir = tempdir().unwrap(); + + // Create a file outside the workspace + let file_path = other_dir.path().join("outside.rs"); + fs::write(&file_path, "test").unwrap(); + + let result = compute_relative_path(workspace.path(), &file_path); + // Should return the original path since it's outside workspace + assert!(result.contains("outside.rs")); + } + + #[test] + fn test_compute_relative_path_nonexistent_file() { + let workspace = tempdir().unwrap(); + let file_path = workspace.path().join("nonexistent.rs"); + + let result = compute_relative_path(workspace.path(), &file_path); + // Should return the original path since file doesn't exist + assert!(result.contains("nonexistent.rs")); + } + + #[test] + fn test_compute_relative_path_nested_directory() { + let workspace = tempdir().unwrap(); + let workspace_path = workspace.path(); + + // Create a deeply nested file + let nested_dir = workspace_path.join("src").join("components").join("ui"); + fs::create_dir_all(&nested_dir).unwrap(); + let file_path = nested_dir.join("button.rs"); + fs::write(&file_path, "test").unwrap(); + + let result = compute_relative_path(workspace_path, &file_path); + assert_eq!(result, "src/components/ui/button.rs"); + } +} diff --git a/crates/codebook/src/dictionaries/combined.gen.txt b/crates/codebook/src/dictionaries/combined.gen.txt index 5e90998..1946a6c 100644 --- a/crates/codebook/src/dictionaries/combined.gen.txt +++ b/crates/codebook/src/dictionaries/combined.gen.txt @@ -1142,6 +1142,7 @@ helvetica here heroicons hex +hgroup hh hi hibernate From 873aaaef29d974b14bfb539e975e9eb1a0b5abee Mon Sep 17 00:00:00 2001 From: Bo Lopker Date: Tue, 23 Dec 2025 13:02:13 -0800 Subject: [PATCH 6/6] Cache workspace canonicalize --- crates/codebook-lsp/src/lsp.rs | 75 +++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/crates/codebook-lsp/src/lsp.rs b/crates/codebook-lsp/src/lsp.rs index 4ed4adb..c34e54f 100644 --- a/crates/codebook-lsp/src/lsp.rs +++ b/crates/codebook-lsp/src/lsp.rs @@ -28,27 +28,37 @@ const SOURCE_NAME: &str = "Codebook"; /// Computes the relative path of a file from a workspace directory. /// Returns the relative path if the file is within the workspace, otherwise returns the absolute path. -fn compute_relative_path(workspace_dir: &Path, file_path: &Path) -> String { - let absolute_workspace_dir = workspace_dir.canonicalize(); - - match absolute_workspace_dir { - Ok(dir) => match file_path.canonicalize() { - Ok(canon_file_path) => match canon_file_path.strip_prefix(&dir) { - Ok(relative) => relative.to_string_lossy().to_string(), - Err(_) => file_path.to_string_lossy().to_string(), - }, +/// If `workspace_dir_canonical` is provided, skips canonicalizing the workspace directory (optimization). +fn compute_relative_path( + workspace_dir: &Path, + workspace_dir_canonical: Option<&Path>, + file_path: &Path, +) -> String { + let workspace_canonical = match workspace_dir_canonical { + Some(dir) => dir.to_path_buf(), + None => match workspace_dir.canonicalize() { + Ok(dir) => dir, + Err(err) => { + info!("Could not canonicalize workspace directory. Error: {err}."); + return file_path.to_string_lossy().to_string(); + } + }, + }; + + match file_path.canonicalize() { + Ok(canon_file_path) => match canon_file_path.strip_prefix(&workspace_canonical) { + Ok(relative) => relative.to_string_lossy().to_string(), Err(_) => file_path.to_string_lossy().to_string(), }, - Err(err) => { - info!("Could not get absolute path from workspace directory. Error: {err}."); - file_path.to_string_lossy().to_string() - } + Err(_) => file_path.to_string_lossy().to_string(), } } pub struct Backend { client: Client, workspace_dir: PathBuf, + /// Cached canonicalized workspace directory for efficient relative path computation + workspace_dir_canonical: Option, codebook: OnceLock>, config: OnceLock>, document_cache: TextDocumentCache, @@ -360,9 +370,11 @@ impl LanguageServer for Backend { impl Backend { pub fn new(client: Client, workspace_dir: &Path) -> Self { + let workspace_dir_canonical = workspace_dir.canonicalize().ok(); Self { client, workspace_dir: workspace_dir.to_path_buf(), + workspace_dir_canonical, codebook: OnceLock::new(), config: OnceLock::new(), document_cache: TextDocumentCache::default(), @@ -473,7 +485,11 @@ impl Backend { } }; let file_path = parsed_uri.to_file_path().unwrap_or_default(); - Some(compute_relative_path(&self.workspace_dir, &file_path)) + Some(compute_relative_path( + &self.workspace_dir, + self.workspace_dir_canonical.as_deref(), + &file_path, + )) } fn add_ignore_file(&self, config: &CodebookConfigFile, file_uri: &str) -> bool { @@ -558,7 +574,11 @@ impl Backend { debug!("Spell-checking file: {file_path:?}"); // Compute relative path for ignore pattern matching - let relative_path = compute_relative_path(&self.workspace_dir, &file_path); + let relative_path = compute_relative_path( + &self.workspace_dir, + self.workspace_dir_canonical.as_deref(), + &file_path, + ); // Convert utf8 byte offsets to utf16 let offsets = StringOffsets::::new(&doc.text); @@ -623,7 +643,24 @@ mod tests { let file_path = subdir.join("test.rs"); fs::write(&file_path, "test").unwrap(); - let result = compute_relative_path(workspace_path, &file_path); + let result = compute_relative_path(workspace_path, None, &file_path); + assert_eq!(result, "src/test.rs"); + } + + #[test] + fn test_compute_relative_path_with_cached_canonical() { + let workspace = tempdir().unwrap(); + let workspace_path = workspace.path(); + let workspace_canonical = workspace_path.canonicalize().unwrap(); + + // Create a file inside the workspace + let subdir = workspace_path.join("src"); + fs::create_dir_all(&subdir).unwrap(); + let file_path = subdir.join("test.rs"); + fs::write(&file_path, "test").unwrap(); + + // Using cached canonical path should produce the same result + let result = compute_relative_path(workspace_path, Some(&workspace_canonical), &file_path); assert_eq!(result, "src/test.rs"); } @@ -636,7 +673,7 @@ mod tests { let file_path = other_dir.path().join("outside.rs"); fs::write(&file_path, "test").unwrap(); - let result = compute_relative_path(workspace.path(), &file_path); + let result = compute_relative_path(workspace.path(), None, &file_path); // Should return the original path since it's outside workspace assert!(result.contains("outside.rs")); } @@ -646,7 +683,7 @@ mod tests { let workspace = tempdir().unwrap(); let file_path = workspace.path().join("nonexistent.rs"); - let result = compute_relative_path(workspace.path(), &file_path); + let result = compute_relative_path(workspace.path(), None, &file_path); // Should return the original path since file doesn't exist assert!(result.contains("nonexistent.rs")); } @@ -662,7 +699,7 @@ mod tests { let file_path = nested_dir.join("button.rs"); fs::write(&file_path, "test").unwrap(); - let result = compute_relative_path(workspace_path, &file_path); + let result = compute_relative_path(workspace_path, None, &file_path); assert_eq!(result, "src/components/ui/button.rs"); } }