From 1b5369ec66b4c4dc299492b2764046feaf1b5602 Mon Sep 17 00:00:00 2001 From: kunlinglio <142668432+kunlinglio@users.noreply.github.com> Date: Fri, 8 May 2026 16:29:50 +0800 Subject: [PATCH 1/7] Refactor ClientMetadata to ClientInfo --- src/client.rs | 53 ------------------------- src/editor_info.rs | 51 ++++++++++++++++++++++++ src/lib.rs | 3 +- src/providers/document_link.rs | 5 +-- src/server.rs | 71 +++++++++++++--------------------- src/server_info.rs | 37 ++++++++++++++++++ 6 files changed, 117 insertions(+), 103 deletions(-) delete mode 100644 src/client.rs create mode 100644 src/editor_info.rs create mode 100644 src/server_info.rs diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 61795b7..0000000 --- a/src/client.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Module to manage environment about client -use std::fmt::Display; -use std::sync::OnceLock; - -use tokio::sync::RwLock; - -use strum_macros::{Display, EnumString}; - -#[derive(EnumString, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] -pub enum Editor { - Zed, - VSCode, - #[strum(default)] - Unknown(String), -} - -#[derive(Debug, Clone)] -pub struct ClientMetadata { - pub editor: Editor, - pub support_document_link: bool, -} - -impl Default for ClientMetadata { - fn default() -> Self { - Self { - editor: Editor::Unknown("unknown".into()), - support_document_link: true, - } - } -} - -impl Display for ClientMetadata { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "\n editor: {}\n support_document_link: {}", - self.editor, self.support_document_link - ) - } -} - -static ENV: OnceLock> = OnceLock::new(); - -pub async fn set_client(client: ClientMetadata) { - let lock = ENV.get_or_init(|| RwLock::new(ClientMetadata::default())); - let mut guard = lock.write().await; - *guard = client; -} - -pub async fn get_client() -> ClientMetadata { - let lock = ENV.get_or_init(|| RwLock::new(ClientMetadata::default())); - lock.read().await.clone() -} diff --git a/src/editor_info.rs b/src/editor_info.rs new file mode 100644 index 0000000..34ea71c --- /dev/null +++ b/src/editor_info.rs @@ -0,0 +1,51 @@ +use std::fmt::Display; + +use tower_lsp_server::ls_types; + +use strum_macros::{Display, EnumString}; + +#[derive(EnumString, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] +pub enum Editor { + Zed, + VSCode, + #[strum(default)] + Unknown(String), +} + +#[derive(Debug, Clone)] +pub struct EditorInfo { + pub editor: Editor, + pub support_document_link: bool, +} + +impl Display for EditorInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "\n editor: {}\n support_document_link: {}", + self.editor, self.support_document_link + ) + } +} + +impl EditorInfo { + pub fn from_initialize_params(params: &ls_types::InitializeParams) -> EditorInfo { + let editor = params + .initialization_options + .as_ref() + .and_then(|options| options.get("editor")) + .and_then(|editor| editor.as_str()) + .map(Editor::from) + .unwrap_or_else(|| Editor::Unknown("unknown".into())); + let support_document_link = params + .capabilities + .text_document + .as_ref() + .and_then(|td| td.document_link.clone()) + .is_some(); + EditorInfo { + editor, + support_document_link, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 0d5095e..20ac42f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,9 +4,9 @@ //! //! This crate is primarily designed to be distributed as a standalone binary. And has no intention to maintain as a library dependency for other projects for now. -mod client; mod config; mod document; +mod editor_info; mod error; mod fs; mod logger; @@ -14,6 +14,7 @@ mod parser; mod providers; mod resolver; mod server; +mod server_info; #[doc(hidden)] pub use crate::server::PathServer; #[doc(hidden)] diff --git a/src/providers/document_link.rs b/src/providers/document_link.rs index 0fe52ec..cca1fdc 100644 --- a/src/providers/document_link.rs +++ b/src/providers/document_link.rs @@ -1,6 +1,5 @@ use tower_lsp_server::ls_types; -use crate::client::get_client; use crate::config::Config; use crate::document::Document; use crate::error::*; @@ -13,9 +12,7 @@ pub async fn provide_document_links( config: &Config, workspace_roots: &[String], ) -> PathServerResult> { - let client = get_client().await; - assert!(client.support_document_link); - assert!(config.highlight.enable); // these should be checked by server + assert!(config.highlight.enable); // this should be checked by server let tokens = resolver::resolve_all(doc, config, workspace_roots, doc_parent).await?; let filtered = tokens .iter() diff --git a/src/server.rs b/src/server.rs index 27a7b10..deb99c1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,30 +1,32 @@ use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; +use std::sync::OnceLock; use std::time::Instant; use tokio::sync::RwLock; use tower_lsp_server::jsonrpc; use tower_lsp_server::ls_types; -use crate::client::{ClientMetadata, Editor, get_client, set_client}; use crate::config; use crate::document::Document; +use crate::editor_info::EditorInfo; use crate::error::*; use crate::fs; use crate::logger::{self}; use crate::parser::tree_sitter_supported; use crate::providers; +use crate::server_info::ServerInfo; use crate::{lsp_debug, lsp_error, lsp_info}; -const VERSION: &str = env!("CARGO_PKG_VERSION"); - #[derive(Debug)] pub struct PathServer { client: tower_lsp_server::Client, workspace_roots: RwLock>, /// file path -> document documents: RwLock>, + editor_info: OnceLock, + server_info: OnceLock, config_cache: RwLock>>, } @@ -35,6 +37,8 @@ impl PathServer { client, workspace_roots: RwLock::new(HashSet::new()), documents: RwLock::new(HashMap::new()), + editor_info: OnceLock::new(), + server_info: OnceLock::new(), config_cache: RwLock::new(None), } } @@ -54,33 +58,6 @@ impl PathServer { *guard = Some(Arc::new(cfg)); } - pub fn parse_editor_env(&self, params: &ls_types::InitializeParams) -> Editor { - let Some(options) = ¶ms.initialization_options else { - return Editor::Unknown("unknown".into()); - }; - let Some(editor) = options.get("editor") else { - return Editor::Unknown("unknown".into()); - }; - if let Some(editor_str) = editor.as_str() { - return Editor::from(editor_str); - } - Editor::Unknown("unknown".into()) - } - - pub fn parse_client_env(&self, params: &ls_types::InitializeParams) -> ClientMetadata { - let editor = self.parse_editor_env(params); - let support_document_link = params - .capabilities - .text_document - .as_ref() - .and_then(|td| td.document_link.clone()) - .is_some(); - ClientMetadata { - editor, - support_document_link, - } - } - pub async fn workspace_paths(&self) -> Vec { let lock_guard = self.workspace_roots.read().await; lock_guard @@ -104,14 +81,18 @@ impl tower_lsp_server::LanguageServer for PathServer { params: ls_types::InitializeParams, ) -> jsonrpc::Result { lsp_info!("Initializing Path Server...").await; - // set editor env - let client_env = self.parse_client_env(¶ms); - lsp_info!("Client Env: {}", client_env).await; - set_client(client_env).await; + // set editor info + let editor_info = EditorInfo::from_initialize_params(¶ms); + lsp_info!("Editor Info: {}", editor_info).await; + self.editor_info.set(editor_info).unwrap(); + // set server info + let server_info = ServerInfo::new(); + lsp_info!("Server Info: {}", server_info).await; + self.server_info.set(server_info).unwrap(); // get workspace roots - // for backward compatibility #[allow(deprecated)] if let Some(uri) = params.root_uri { + // for backward compatibility let mut roots = self.workspace_roots.write().await; roots.insert(uri); } @@ -163,12 +144,6 @@ impl tower_lsp_server::LanguageServer for PathServer { async fn initialized(&self, _: ls_types::InitializedParams) { lsp_info!("Path Server initialized").await; - lsp_info!("Path Server version: {}", VERSION).await; - if cfg!(debug_assertions) { - lsp_info!("Running in debug mode").await; - } else { - lsp_info!("Running in release mode").await; - } } async fn shutdown(&self) -> jsonrpc::Result<()> { @@ -330,8 +305,11 @@ impl tower_lsp_server::LanguageServer for PathServer { ) -> jsonrpc::Result>> { let start = Instant::now(); let config = self.get_config().await; - let client = get_client().await; - if !client.support_document_link { + let editor_info = self + .editor_info + .get() + .expect("Editor info must be initialized"); + if !editor_info.support_document_link { lsp_info!("[Document Link] Client does not support document link").await; return Ok(None); }; @@ -447,9 +425,12 @@ impl tower_lsp_server::LanguageServer for PathServer { params.text_document_position_params.position.character ) .await; - let client = get_client().await; + let editor_info = self + .editor_info + .get() + .expect("Editor info must be initialized"); let config = self.get_config().await; - if client.support_document_link && config.highlight.enable { + if editor_info.support_document_link && config.highlight.enable { lsp_info!("[Hover] Client support document link and highlight is enabled, provide nothing to avoid duplicated hover item in {:?}", start.elapsed()).await; return Ok(None); }; diff --git a/src/server_info.rs b/src/server_info.rs new file mode 100644 index 0000000..d5c3112 --- /dev/null +++ b/src/server_info.rs @@ -0,0 +1,37 @@ +use std::fmt::Display; + +use strum_macros::{Display, EnumString}; + +#[derive(EnumString, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] +pub enum ServerMode { + Debug, + Release, +} + +#[derive(Debug, Clone)] +pub struct ServerInfo { + version: String, + mode: ServerMode, +} + +impl Display for ServerInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "\n version: {}\n mode: {}", + self.version, self.mode + ) + } +} + +impl ServerInfo { + pub fn new() -> ServerInfo { + let version = env!("CARGO_PKG_VERSION").to_string(); + let mode = if cfg!(debug_assertions) { + ServerMode::Debug + } else { + ServerMode::Release + }; + ServerInfo { version, mode } + } +} From 66cdfc53591efdd5956e22a01a56f692e1058fdb Mon Sep 17 00:00:00 2001 From: kunlinglio <142668432+kunlinglio@users.noreply.github.com> Date: Thu, 21 May 2026 10:23:53 +0800 Subject: [PATCH 2/7] Add todo: support for tokenization by `:` in docker-compose --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ca26f92..f7c4ff8 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Run `zed: open settings file` from the command palette to edit user settings jso - [x] Support path highlight. - [x] Support remote window. - [x] Improve path extraction precision. +- [ ] Support tokenize by `:` in docker-compose. - [ ] **Zed**: Support all language by use "wildcard" in extension.toml (Waiting for Zed extension api support) ## Development From 67fd2ecc5037873b455300f5eb03c05f00ccb08d Mon Sep 17 00:00:00 2001 From: kunlinglio <142668432+kunlinglio@users.noreply.github.com> Date: Mon, 25 May 2026 08:58:22 +0800 Subject: [PATCH 3/7] Enhance regex parser to support bare path tokens --- CHANGELOG.md | 7 +++++++ src/document/language.rs | 2 ++ src/parser/path/regex.rs | 23 +++++++++++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bacb6f..b9b7172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the Path Server will be documented in this file. +## [Unreleased] +### Added +- **Core**: Added support for parsing bare path tokens delimited by whitespace (e.g., `src/ui/menu.ts` in YAML or other non-string contexts). + +### Improved +- **Core**: Refactored the `ClientMetadata` structure for better logging. + ## [1.2.0] - 2026-05-08 Path Server is now published to crates.io! You can now install it via `cargo install path-server`. diff --git a/src/document/language.rs b/src/document/language.rs index 778be8e..9a33ab9 100644 --- a/src/document/language.rs +++ b/src/document/language.rs @@ -23,6 +23,8 @@ pub enum Language { dart, deno, dockerfile, + #[strum(serialize = "dockercompose")] + docker_compose, elixir, elm, emmet, diff --git a/src/parser/path/regex.rs b/src/parser/path/regex.rs index 0b0d832..85e5453 100644 --- a/src/parser/path/regex.rs +++ b/src/parser/path/regex.rs @@ -9,6 +9,7 @@ pub fn extract_string(document: &Document) -> Option> { r#""(?:[^"\\]|\\.)*""#, // match string in double quote, support escaped \" r#"'(?:[^'\\]|\\.)*'"#, // match string in single quote, support escaped \' r#"`(?:[^`\\]|\\.)*`"#, // match string in back tick, and support escaped \` + r#"\S*[/\\]\S*"#, // match token warped by space and contain at least one slash or backslash ]; let regex = Regex::new(&string_regexes.join("|")) .map_err(|e| PathServerError::Unknown(format!("Failed to compile regex expression: {}", e))) @@ -16,13 +17,22 @@ pub fn extract_string(document: &Document) -> Option> { let mut strings = vec![]; for matched in regex.find_iter(&document.text) { let content = matched.as_str(); - // slice quotes - if content.len() >= 2 { + let first_char = content.chars().next(); + let is_quoted = matches!(first_char, Some('"' | '\'' | '`')); + if is_quoted && content.len() >= 2 { + // strip surrounding quotes for quoted strings strings.push(PathCandidate { content: content[1..content.len() - 1].to_string(), start_byte: matched.start() + 1, end_byte: matched.end() - 1, }) + } else if !is_quoted && !content.is_empty() { + // bare path tokens — use the match as-is + strings.push(PathCandidate { + content: content.to_string(), + start_byte: matched.start(), + end_byte: matched.end(), + }) } } Some(strings) @@ -44,6 +54,15 @@ mod tests { assert_eq!(res.len(), 3); } + #[test] + fn test_extract_bare_path_tokens() { + let src = r#" - id: meta-lint-ci + name: Running src/ui/menu.ts to ensure changes will pass linting on CI"#; + let doc = Document::new(src.to_string(), "yaml").expect("failed to create Document"); + let res = extract_string(&doc).unwrap_or_default(); + assert!(res.iter().any(|p| p.content == "src/ui/menu.ts")); + } + #[test] fn test_extract_no_strings() { let src = "let x = 42;"; From 5bc32a1f35f07bf92ae5f0a6ef37b129ff934cca Mon Sep 17 00:00:00 2001 From: kunlinglio <142668432+kunlinglio@users.noreply.github.com> Date: Mon, 25 May 2026 09:06:51 +0800 Subject: [PATCH 4/7] Add support for parsing colon-delimited paths --- CHANGELOG.md | 1 + src/parser/path/mod.rs | 70 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9b7172..bdc13b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to the Path Server will be documented in this file. ## [Unreleased] ### Added - **Core**: Added support for parsing bare path tokens delimited by whitespace (e.g., `src/ui/menu.ts` in YAML or other non-string contexts). +- **Core**: Added support for parsing paths from colon-delimited tokens, enabling docker-compose volume mount path extraction (e.g., `./src:/app/src`). ### Improved - **Core**: Refactored the `ClientMetadata` structure for better logging. diff --git a/src/parser/path/mod.rs b/src/parser/path/mod.rs index 34200dc..1c6020b 100644 --- a/src/parser/path/mod.rs +++ b/src/parser/path/mod.rs @@ -41,6 +41,11 @@ fn extract_paths_from_string(path_ref: PathCandidate) -> Vec { // Level 2: the part of string (split by space) is a path or not results.extend(path_ref.split(&[' ', '\n'])); + // Level 3: the part of string (split by colon) is a path or not + // Handles docker-compose volume mounts (e.g., ./src:/app/src) + // and PATH-like environment variables (e.g., /usr/bin:/usr/local/bin) + results.extend(path_ref.split(&[':', ':'])); + results } @@ -87,6 +92,71 @@ mod tests { assert!(res.iter().any(|p| p.content == "/var/log/syslog")); } + #[test] + fn test_extract_paths_from_volume_mount() { + // docker-compose volume mount: host_path:container_path + let candidate = PathCandidate { + content: "./src:/app/src".to_string(), + start_byte: 0, + end_byte: 14, + }; + let res = extract_paths_from_string(candidate); + for p in &res { + eprintln!("Extracted: {};", p.content); + } + assert!(res.iter().any(|p| p.content == "./src")); + assert!(res.iter().any(|p| p.content == "/app/src")); + } + + #[test] + fn test_extract_paths_from_absolute_volume_mount() { + let candidate = PathCandidate { + content: "/host/path:/container/path".to_string(), + start_byte: 0, + end_byte: 25, + }; + let res = extract_paths_from_string(candidate); + for p in &res { + eprintln!("Extracted: {};", p.content); + } + assert!(res.iter().any(|p| p.content == "/host/path")); + assert!(res.iter().any(|p| p.content == "/container/path")); + } + + #[test] + fn test_extract_paths_from_volume_mount_with_readonly() { + // docker-compose volume mount with :ro mode flag + let candidate = PathCandidate { + content: "./data:/app/data:ro".to_string(), + start_byte: 0, + end_byte: 18, + }; + let res = extract_paths_from_string(candidate); + for p in &res { + eprintln!("Extracted: {};", p.content); + } + assert!(res.iter().any(|p| p.content == "./data")); + assert!(res.iter().any(|p| p.content == "/app/data")); + // "ro" should NOT be extracted (no path separator) + assert!(!res.iter().any(|p| p.content == "ro")); + } + + #[test] + fn test_extract_paths_from_windows_absolute_not_broken() { + // Windows absolute path C:\Users\file.txt should not be broken by colon split + let candidate = PathCandidate { + content: "C:\\Users\\file.txt".to_string(), + start_byte: 0, + end_byte: 17, + }; + let res = extract_paths_from_string(candidate); + for p in &res { + eprintln!("Extracted: {};", p.content); + } + // The whole path should be present + assert!(res.iter().any(|p| p.content == "C:\\Users\\file.txt")); + } + #[test] fn test_extract_paths_with_trailing_spaces() { let candidate = PathCandidate { From 1652f4d9d6b2b43352ff57a781f94e6435abe7a9 Mon Sep 17 00:00:00 2001 From: Kunling Liu <142668432+kunlinglio@users.noreply.github.com> Date: Mon, 25 May 2026 09:13:19 +0800 Subject: [PATCH 5/7] Fix typo Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/parser/path/regex.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/path/regex.rs b/src/parser/path/regex.rs index 85e5453..f028f8e 100644 --- a/src/parser/path/regex.rs +++ b/src/parser/path/regex.rs @@ -9,7 +9,7 @@ pub fn extract_string(document: &Document) -> Option> { r#""(?:[^"\\]|\\.)*""#, // match string in double quote, support escaped \" r#"'(?:[^'\\]|\\.)*'"#, // match string in single quote, support escaped \' r#"`(?:[^`\\]|\\.)*`"#, // match string in back tick, and support escaped \` - r#"\S*[/\\]\S*"#, // match token warped by space and contain at least one slash or backslash + r#"\S*[/\\]\S*"#, // match token wrapped by space and contain at least one slash or backslash ]; let regex = Regex::new(&string_regexes.join("|")) .map_err(|e| PathServerError::Unknown(format!("Failed to compile regex expression: {}", e))) From c4ecad0f0e058696617ba2eb4ea626fb80c08c74 Mon Sep 17 00:00:00 2001 From: Kunling Liu <142668432+kunlinglio@users.noreply.github.com> Date: Mon, 25 May 2026 09:16:52 +0800 Subject: [PATCH 6/7] Fix changelog in consistency Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdc13b2..f964849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to the Path Server will be documented in this file. - **Core**: Added support for parsing paths from colon-delimited tokens, enabling docker-compose volume mount path extraction (e.g., `./src:/app/src`). ### Improved -- **Core**: Refactored the `ClientMetadata` structure for better logging. +- **Core**: Replaced `ClientMetadata` with `EditorInfo`/`ServerInfo` for better logging. ## [1.2.0] - 2026-05-08 Path Server is now published to crates.io! You can now install it via `cargo install path-server`. From 95992e67cf9e9dac3ad293688941ad8854370e7a Mon Sep 17 00:00:00 2001 From: kunlinglio <142668432+kunlinglio@users.noreply.github.com> Date: Mon, 25 May 2026 09:17:54 +0800 Subject: [PATCH 7/7] Fix end_byte calculation in path extraction --- README.md | 1 - src/parser/path/mod.rs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7c4ff8..ca26f92 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,6 @@ Run `zed: open settings file` from the command palette to edit user settings jso - [x] Support path highlight. - [x] Support remote window. - [x] Improve path extraction precision. -- [ ] Support tokenize by `:` in docker-compose. - [ ] **Zed**: Support all language by use "wildcard" in extension.toml (Waiting for Zed extension api support) ## Development diff --git a/src/parser/path/mod.rs b/src/parser/path/mod.rs index 1c6020b..b8aa706 100644 --- a/src/parser/path/mod.rs +++ b/src/parser/path/mod.rs @@ -44,7 +44,7 @@ fn extract_paths_from_string(path_ref: PathCandidate) -> Vec { // Level 3: the part of string (split by colon) is a path or not // Handles docker-compose volume mounts (e.g., ./src:/app/src) // and PATH-like environment variables (e.g., /usr/bin:/usr/local/bin) - results.extend(path_ref.split(&[':', ':'])); + results.extend(path_ref.split(&[':'])); results } @@ -113,7 +113,7 @@ mod tests { let candidate = PathCandidate { content: "/host/path:/container/path".to_string(), start_byte: 0, - end_byte: 25, + end_byte: 26, }; let res = extract_paths_from_string(candidate); for p in &res { @@ -129,7 +129,7 @@ mod tests { let candidate = PathCandidate { content: "./data:/app/data:ro".to_string(), start_byte: 0, - end_byte: 18, + end_byte: 19, }; let res = extract_paths_from_string(candidate); for p in &res {