LS: support glob ignores using pathspec

This commit is contained in:
Michael Panchenko committed 2025-04-07 15:43:41 +02:00
1 parent 8e68e2f4c3
commit c4384682f2
5 files changed
+71 -16

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ dependencies = [
"pyyaml>=6.0.2",
"jinja2>=3.1.6",
"dotenv>=0.9.9",
"pathspec>=0.12.1",
]
[project.scripts]
+38 -16
View File
@@ -16,14 +16,15 @@ import pickle
import re
import threading
from collections import defaultdict
from urllib.parse import urlparse
from contextlib import asynccontextmanager, contextmanager
from copy import copy
from typing import Any, AsyncGenerator, Dict, Generator, List, Optional, Set, Tuple, Union
from typing import Dict, List, Optional, Tuple, Union
from fnmatch import fnmatch
from pathlib import Path, PurePath
from typing import AsyncIterator, Dict, Iterator, List, Optional, Tuple, Union, cast
import pathspec
from serena.text_utils import LineType, MatchedConsecutiveLines, TextLine, search_text
from . import multilspy_types
from .lsp_protocol_handler import lsp_types as LSPTypes
@@ -181,14 +182,6 @@ class LanguageServer:
self.repository_root_path: str = repository_root_path
self.completions_available = asyncio.Event()
# for all absolute paths in ignored_dirs, convert them to relative paths
self.ignored_paths = []
for path in set(config.ignored_paths):
if os.path.isabs(path):
path = str(Path(path).resolve().relative_to(self.repository_root_path))
self.ignored_paths.append(path)
if config.trace_lsp_communication:
def logging_fn(source, target, msg):
@@ -213,27 +206,56 @@ class LanguageServer:
self._cache_has_changed = bool
self.language = Language(language_id)
# Helper function to check if a relative path should be ignored
# Set up the pathspec matcher for the ignored paths
# for all absolute paths in ignored_paths, convert them to relative paths
processed_patterns = []
for pattern in set(config.ignored_paths):
# Normalize separators (pathspec expects forward slashes)
pattern = pattern.replace(os.path.sep, '/')
processed_patterns.append(pattern)
# Create a pathspec matcher from the processed patterns
self.ignore_spec = pathspec.PathSpec.from_lines(
pathspec.patterns.GitWildMatchPattern,
processed_patterns
)
def should_ignore_path(self, relative_path: str) -> bool:
"""
Determine if a path should be ignored based on file type
and ignore patterns.
:param relative_path: Relative path to check
:return: True if the path should be ignored, False otherwise
"""
# Check file extension if it's a file
fn_matcher = self.language.get_source_fn_matcher()
if os.path.isfile(relative_path) and not fn_matcher.is_relevant_filename(relative_path):
return True
# Normalize path separators for consistent comparison
# Create normalized path for consistent handling
rel_path = Path(relative_path)
for ignored_path in self.ignored_paths:
if rel_path.is_relative_to(ignored_path):
return True
# Check each part of the path against always fulfilled ignore conditions
for part in rel_path.parts:
if not part: # Skip empty parts (e.g., from leading '/')
if not part: # Skip empty parts (e.g., from leading '/')
continue
# Check standard ignores
if self.should_always_ignore(part):
return True
# Use pathspec for gitignore-style pattern matching
# Normalize path separators for pathspec (it expects forward slashes)
normalized_path = str(rel_path).replace(os.path.sep, '/')
# Use the pathspec matcher to check if the path matches any ignore pattern
if self.ignore_spec.match_file(normalized_path):
return True
return False
@asynccontextmanager
async def start_server(self) -> AsyncIterator["LanguageServer"]:
"""
+5
View File
@@ -88,6 +88,11 @@ class PathUtils:
parsed = urlparse(uri)
host = "{0}{0}{mnt}{0}".format(os.path.sep, mnt=parsed.netloc)
return os.path.normpath(os.path.join(host, url2pathname(unquote(parsed.path))))
@staticmethod
def is_glob_pattern(pattern: str) -> bool:
"""Check if a pattern contains glob-specific characters."""
return any(c in pattern for c in '*?[]!')
class FileUtils:
"""
@@ -44,3 +44,28 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer):
# assert that scripts does not appear in the references
assert not any("scripts" in ref["relativePath"] for ref in references)
def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
"""Tests that refs and symbols with glob patterns are ignored."""
config = MultilspyConfig(
code_language=Language.PYTHON,
trace_lsp_communication=False,
ignored_paths=["*ipts", "custom_t*"],
)
logger = MultilspyLogger()
ls = SyncLanguageServer.create(config, logger, str(repo_path))
ls.start()
# same as in the above tests
root = ls.request_full_symbol_tree()[0]
root_children = root["children"]
children_names = {child["name"] for child in root_children}
assert children_names == {"test_repo", "examples"}
# test that the refs and symbols with glob patterns are ignored
definition_file = "test_repo/models.py"
definition_line = 56
definition_col = 6
references = ls.request_references(definition_file, definition_line, definition_col)
assert not any("scripts" in ref["relativePath"] for ref in references)
Generated
+2
View File
@@ -950,6 +950,7 @@ dependencies = [
{ name = "jinja2" },
{ name = "mcp" },
{ name = "overrides" },
{ name = "pathspec" },
{ name = "pydantic" },
{ name = "pyright" },
{ name = "python-dotenv" },
@@ -997,6 +998,7 @@ requires-dist = [
{ name = "mcp", specifier = ">=1.5.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.4.1" },
{ name = "overrides", specifier = ">=7.7.0,<8" },
{ name = "pathspec", specifier = ">=0.12.1" },
{ name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.20.0" },
{ name = "pydantic", specifier = ">=2.10.6" },
{ name = "pyright", specifier = ">=1.1.396,<2" },