LS: option to add_gitignore_content_to_config

Defaults to true since it's probably always a good idea
This commit is contained in:
Michael Panchenko committed 2025-04-07 16:44:25 +02:00
1 parent c3e1d0d9ac
commit 54588962b7
4 files changed
+187 -13

No files matched your search

+40 -11
View File
@@ -81,18 +81,17 @@ class LSPFileBuffer:
class LanguageServer:
# To be overridden and extended by subclasses
def should_always_ignore(self, dirname: str) -> bool:
return dirname.startswith('.')
"""
The LanguageServer class provides a language agnostic interface to the Language Server Protocol.
It is used to communicate with Language Servers of different programming languages.
"""
# To be overridden and extended by subclasses
def should_always_ignore(self, dirname: str) -> bool:
return dirname.startswith('.')
@classmethod
def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer":
def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, add_gitignore_content_to_config: bool = True) -> "LanguageServer":
"""
Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language.
@@ -102,9 +101,29 @@ class LanguageServer:
:param repository_root_path: The root path of the repository.
:param config: The Multilspy configuration.
:param logger: The logger to use.
:param add_gitignore_content_to_config: whether to add the content of the .gitignore file (if any found) to the config, so that
the paths ignored there are also ignored by the language server
:return LanguageServer: A language specific LanguageServer instance.
"""
config = copy(config) # prevent mutation
if add_gitignore_content_to_config:
gitignore_path = os.path.join(repository_root_path, ".gitignore")
if not os.path.exists(gitignore_path):
logger.log(
f"Should ignore all files in gitignore not not .gitignore found at {gitignore_path}. Skipping.",
logging.WARNING
)
gitignore_file_content = None
else:
if config.gitignore_file_content is not None:
raise ValueError(
f"Asked to add gitignore content to the config for {repository_root_path=} but there already is a non-empty entry"
)
with open(gitignore_path) as f:
gitignore_file_content = f.read()
config.gitignore_file_content = gitignore_file_content
if config.code_language == Language.PYTHON:
from multilspy.language_servers.pyright_language_server.pyright_server import (
PyrightServer,
@@ -167,7 +186,7 @@ class LanguageServer:
:param config: The Multilspy configuration.
:param logger: The logger to use.
:param repository_root_path: The root path of the repository.
:param cmd: Each language server has a specific command used to start the server.
:param process_launch_info: Each language server has a specific command used to start the server.
This parameter is the command to launch the language server process.
The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
as opposed to HTTP, TCP modes supported by some language servers.
@@ -213,13 +232,16 @@ class LanguageServer:
# Normalize separators (pathspec expects forward slashes)
pattern = pattern.replace(os.path.sep, '/')
processed_patterns.append(pattern)
# Combine explicitly passed patterns with
if config.gitignore_file_content is not None:
processed_patterns.extend(config.gitignore_file_content.splitlines())
# 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
@@ -249,6 +271,11 @@ class LanguageServer:
# Normalize path separators for pathspec (it expects forward slashes)
normalized_path = str(rel_path).replace(os.path.sep, '/')
# pathspec can't handle the matching of directories if they don't end with a slash!
# see https://github.com/cpburnz/python-pathspec/issues/89
if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith('/'):
normalized_path = normalized_path + '/'
# Use the pathspec matcher to check if the path matches any ignore pattern
if self.ignore_spec.match_file(normalized_path):
return True
@@ -1500,7 +1527,7 @@ class SyncLanguageServer:
@classmethod
def create(
cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str
cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, add_gitignore_content_to_config=True,
) -> "SyncLanguageServer":
"""
Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language.
@@ -1510,10 +1537,12 @@ class SyncLanguageServer:
:param repository_root_path: The root path of the repository.
:param config: The Multilspy configuration.
:param logger: The logger to use.
:param add_gitignore_content_to_config: whether to add the content of the .gitignore file (if any found) to the config, so that
the paths ignored there are also ignored by the language server
:return SyncLanguageServer: A language specific LanguageServer instance.
"""
return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path))
return SyncLanguageServer(LanguageServer.create(config, logger, repository_root_path, add_gitignore_content_to_config=add_gitignore_content_to_config))
@contextmanager
def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]:
+5 -2
View File
@@ -67,7 +67,10 @@ class MultilspyConfig:
"""
code_language: Language
trace_lsp_communication: bool = False
ignored_paths: List[str] = field(default_factory=list)
ignored_paths: list[str] = field(default_factory=list)
"""Paths, dirs or glob-like patterns. The matching will follow the same logic as for .gitignore entries"""
gitignore_file_content: str | None = None
"""Optional content of the gitignore file. If passed, will be used in addition to the explicitly passed ignored_paths for deciding which paths to ignore."""
@classmethod
def from_dict(cls, env: dict):
@@ -78,4 +81,4 @@ class MultilspyConfig:
return cls(**{
k: v for k, v in env.items()
if k in inspect.signature(cls).parameters
})
})
+1
View File
@@ -0,0 +1 @@
ignore_this_dir*/
@@ -0,0 +1,141 @@
"""
Example demonstrating user management with the test_repo module.
This example showcases:
- Creating and managing users
- Using various object types and relationships
- Type annotations and complex Python patterns
"""
import logging
from dataclasses import dataclass
from typing import Any
from test_repo.models import User, create_user_object
from test_repo.services import UserService
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@dataclass
class UserStats:
"""Statistics about user activity."""
user_id: str
login_count: int = 0
last_active_days: int = 0
engagement_score: float = 0.0
def is_active(self) -> bool:
"""Check if the user is considered active."""
return self.last_active_days < 30
class UserManager:
"""Example class demonstrating complex user management."""
def __init__(self, service: UserService):
self.service = service
self.active_users: dict[str, User] = {}
self.user_stats: dict[str, UserStats] = {}
def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User:
"""Register a new user."""
logger.info(f"Registering new user: {name} ({email})")
user = self.service.create_user(name=name, email=email, roles=roles)
self.active_users[user.id] = user
self.user_stats[user.id] = UserStats(user_id=user.id)
return user
def get_user(self, user_id: str) -> User | None:
"""Get a user by ID."""
if user_id in self.active_users:
return self.active_users[user_id]
# Try to fetch from service
user = self.service.get_user(user_id)
if user:
self.active_users[user.id] = user
return user
def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None:
"""Update statistics for a user."""
if user_id not in self.user_stats:
self.user_stats[user_id] = UserStats(user_id=user_id)
stats = self.user_stats[user_id]
stats.login_count = login_count
stats.last_active_days = days_since_active
# Calculate engagement score based on activity
engagement = (100 - min(days_since_active, 100)) * 0.8
engagement += min(login_count, 20) * 0.2
stats.engagement_score = engagement
def get_active_users(self) -> list[User]:
"""Get all active users."""
active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()]
return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users]
def get_user_by_email(self, email: str) -> User | None:
"""Find a user by their email address."""
for user in self.active_users.values():
if user.email == email:
return user
return None
# Example function demonstrating type annotations
def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]:
"""Process user data with optional transformations."""
result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0}
for user in users:
if transform_func:
user_data = transform_func(user.to_dict())
else:
user_data = user.to_dict()
result["users"].append(user_data)
result["total"] += 1
if "admin" in user.roles:
result["admin_count"] += 1
return result
def main():
"""Main function demonstrating the usage of UserManager."""
# Initialize service and manager
service = UserService()
manager = UserManager(service)
# Register some users
admin = manager.register_user("Admin User", "admin@example.com", ["admin"])
user1 = manager.register_user("Regular User", "user@example.com", ["user"])
user2 = manager.register_user("Another User", "another@example.com", ["user"])
# Update some stats
manager.update_user_stats(admin.id, 100, 5)
manager.update_user_stats(user1.id, 50, 10)
manager.update_user_stats(user2.id, 10, 45) # Inactive user
# Get active users
active_users = manager.get_active_users()
logger.info(f"Active users: {len(active_users)}")
# Process user data
user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")})
logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins")
# Example of calling create_user directly
external_user = create_user_object(id="ext123", name="External User", email="external@example.org", roles=["external"])
logger.info(f"Created external user: {external_user.name}")
if __name__ == "__main__":
main()