JetBrainsPluginClient: Generalise path matching

* Account for additional prefixes
  * Account for WSL vs. Windows path mismatch

Add a test for path matching
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-02-15 18:41:10 +01:00
1 parent 448b3693bc
commit 4a0812972f
2 files changed
+67 -19

No files matched your search

+47 -19
View File
@@ -118,43 +118,71 @@ class JetBrainsPluginClient(ToStringMixin):
if client.matches(resolved_path):
return client
plugin_paths_found = []
for port in range(cls.BASE_PORT, cls.BASE_PORT + 20):
client = JetBrainsPluginClient(port)
if client.matches(resolved_path):
log.info("Found matching %s", client)
cls._last_port = port
return client
elif client._project_root is not None:
plugin_paths_found.append(client._project_root)
log.warning(
"Searched for Serena JetBrains plugin service for project at %s but found no matching service. "
"Found plugin instances for the following project paths: %s",
resolved_path,
plugin_paths_found,
)
raise ServerNotFoundError("Found no Serena service in a JetBrains IDE instance for the project at " + str(resolved_path))
@staticmethod
def _normalize_wsl_path(path_str: str) -> Path:
def _paths_match(resolved_serena_path: str, plugin_path: str) -> bool:
"""
Normalize WSL UNC paths to Linux paths for comparison.
Checks whether the resolved Serena path matches the plugin path, accounting for possible prefixes
in the plugin path, different file system perspectives, and case sensitivity.
When JetBrains IDE runs on Windows with a project opened from WSL,
it returns paths like `//wsl.localhost/Ubuntu-24.04/home/user/project`
or `//wsl$/Ubuntu/home/user/project`. This method converts such paths
to standard Linux format `/home/user/project` for proper matching.
Concrete aspects considered:
- The plugin path may contain prefixes:
- The plugin path may be a WSL UNC path, e.g. `//wsl.localhost/Ubuntu-24.04/home/user/project`
or `//wsl$/Ubuntu/home/user/project` while Serena will just have `/home/user/project`
- Other prefixes like `/workspaces/serena/C:/Users/user/projects/my-app`
- One path may use a different file system perspective (particularly WSL vs Windows-native) but still
point to the same location, e.g. `/mnt/c/` vs `C:/`
- Case sensitivity
:param path_str: Path string that may be a WSL UNC path
:return: Normalized Path object
:param resolved_serena_path: The resolved project root path from Serena's perspective
:param plugin_path: The project root path reported by the plugin (which may be a WSL UNC path)
:return: True if the paths match, False otherwise
"""
path_str = str(path_str)
# Match //wsl.localhost/<distro>/... or //wsl$/<distro>/...
match = re.match(r"^//wsl(?:\.localhost|\$)/[^/]+(.*)$", path_str, re.IGNORECASE)
if match:
return Path(match.group(1))
return Path(path_str)
# try to resolve the plugin path, checking for a direct match
# (this is robust against symlinks as long as there are no prefixes)
try:
resolved_plugin_path = str(Path(plugin_path).resolve())
if resolved_plugin_path == resolved_serena_path:
return True
except:
pass
def normalise_wsl_mnt(path_str: str) -> str:
# normalise WSL /mnt/c/ to c:/ for comparison
return re.sub(r"/mnt/([a-z])/", r"\1:/", path_str, flags=re.IGNORECASE)
# standardise paths for comparison: normalise WSL /mnt/ to Windows paths and ignore case
std_serena_path = normalise_wsl_mnt(str(resolved_serena_path)).lower()
std_plugin_path = normalise_wsl_mnt(str(plugin_path)).lower()
# At this point, the plugin path may still contain prefixes, so we check if the Serena path is a suffix of the plugin path
return std_plugin_path.endswith(std_serena_path)
def matches(self, resolved_path: Path) -> bool:
"""
:param resolved_path: the resolved project root path from Serena's perspective
:return: whether this client instance matches the given project path
"""
if self._project_root is None:
return False
try:
plugin_root = self._normalize_wsl_path(self._project_root)
return plugin_root.resolve() == resolved_path
except ConnectionError:
return False
return self._paths_match(str(resolved_path), self._project_root)
def is_version_at_least(self, *version_parts: int) -> bool:
if self._plugin_version is None:
@@ -0,0 +1,20 @@
import pytest
from serena.constants import REPO_ROOT
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
class TestSerenaJetBrainsPluginClient:
@pytest.mark.parametrize(
"serena_path, plugin_path",
[
(REPO_ROOT, REPO_ROOT),
("/home/user/project", "/home/user/project"),
("/home/user/project", "//wsl.localhost/Ubuntu-24.04/home/user/project"),
("/home/user/project", "//wsl$/Ubuntu/home/user/project"),
("/home/user/project", "//wsl$/Ubuntu/home/user/project"),
("/mnt/c/Users/user/projects/my-app", "/workspaces/serena/C:/Users/user/projects/my-app"),
],
)
def test_path_matching(self, serena_path, plugin_path) -> None:
assert JetBrainsPluginClient._paths_match(serena_path, plugin_path)