diff --git a/src/code_index_mcp/search/ag.py b/src/code_index_mcp/search/ag.py index e2506a2..e5c7af5 100644 --- a/src/code_index_mcp/search/ag.py +++ b/src/code_index_mcp/search/ag.py @@ -27,7 +27,8 @@ def search( context_lines: int = 0, file_pattern: Optional[str] = None, fuzzy: bool = False, - regex: bool = False + regex: bool = False, + max_line_length: Optional[int] = None ) -> Dict[str, List[Tuple[int, str]]]: """ Execute a search using The Silver Searcher (ag). @@ -40,6 +41,7 @@ def search( file_pattern: File pattern to filter fuzzy: Enable word boundary matching (not true fuzzy search) regex: Enable regex pattern matching + max_line_length: Optional. Limit the length of lines when context_lines is used """ # ag prints line numbers and groups by file by default, which is good. # --noheading is used to be consistent with other tools' output format. @@ -116,10 +118,10 @@ def search( if process.returncode > 1: raise RuntimeError(f"ag failed with exit code {process.returncode}: {process.stderr}") - return parse_search_output(process.stdout, base_path) + return parse_search_output(process.stdout, base_path, max_line_length) except FileNotFoundError: raise RuntimeError("'ag' (The Silver Searcher) not found. Please install it and ensure it's in your PATH.") except Exception as e: # Re-raise other potential exceptions like permission errors - raise RuntimeError(f"An error occurred while running ag: {e}") + raise RuntimeError(f"An error occurred while running ag: {e}") diff --git a/src/code_index_mcp/search/base.py b/src/code_index_mcp/search/base.py index 038e6b5..679620f 100644 --- a/src/code_index_mcp/search/base.py +++ b/src/code_index_mcp/search/base.py @@ -14,13 +14,18 @@ from ..indexing.qualified_names import normalize_file_path -def parse_search_output(output: str, base_path: str) -> Dict[str, List[Tuple[int, str]]]: +def parse_search_output( + output: str, + base_path: str, + max_line_length: Optional[int] = None +) -> Dict[str, List[Tuple[int, str]]]: """ Parse the output of command-line search tools (grep, ag, rg). Args: output: The raw output from the command-line tool. base_path: The base path of the project to make file paths relative. + max_line_length: Optional maximum line length to truncate long lines. Returns: A dictionary where keys are file paths and values are lists of (line_number, line_content) tuples. @@ -53,6 +58,10 @@ def parse_search_output(output: str, base_path: str) -> Dict[str, List[Tuple[int # Normalize path separators for consistency relative_path = normalize_file_path(relative_path) + # Truncate content if it exceeds max_line_length + if max_line_length and len(content) > max_line_length: + content = content[:max_line_length] + '... (truncated)' + if relative_path not in results: results[relative_path] = [] results[relative_path].append((line_number, content)) @@ -175,7 +184,8 @@ def search( context_lines: int = 0, file_pattern: Optional[str] = None, fuzzy: bool = False, - regex: bool = False + regex: bool = False, + max_line_length: Optional[int] = None ) -> Dict[str, List[Tuple[int, str]]]: """ Execute a search using the specific strategy. @@ -193,4 +203,3 @@ def search( A dictionary mapping filenames to lists of (line_number, line_content) tuples. """ pass - diff --git a/src/code_index_mcp/search/basic.py b/src/code_index_mcp/search/basic.py index 57aab77..c480990 100644 --- a/src/code_index_mcp/search/basic.py +++ b/src/code_index_mcp/search/basic.py @@ -46,7 +46,8 @@ def search( context_lines: int = 0, file_pattern: Optional[str] = None, fuzzy: bool = False, - regex: bool = False + regex: bool = False, + max_line_length: Optional[int] = None ) -> Dict[str, List[Tuple[int, str]]]: """ Execute a basic, line-by-line search. @@ -60,6 +61,7 @@ def search( file_pattern: File pattern to filter fuzzy: Enable word boundary matching regex: Enable regex pattern matching + max_line_length: Optional. Limit the length of lines when context_lines is used """ results: Dict[str, List[Tuple[int, str]]] = {} @@ -94,10 +96,15 @@ def search( with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: for line_num, line in enumerate(f, 1): if search_regex.search(line): + content = line.rstrip('\n') + # Truncate content if it exceeds max_line_length + if max_line_length and len(content) > max_line_length: + content = content[:max_line_length] + '... (truncated)' + if rel_path not in results: results[rel_path] = [] # Strip newline for consistent output - results[rel_path].append((line_num, line.rstrip('\n'))) + results[rel_path].append((line_num, content)) except (UnicodeDecodeError, PermissionError, OSError): # Ignore files that can't be opened or read due to encoding/permission issues continue @@ -105,4 +112,4 @@ def search( # Ignore any other unexpected exceptions to maintain robustness continue - return results \ No newline at end of file + return results diff --git a/src/code_index_mcp/search/grep.py b/src/code_index_mcp/search/grep.py index cd2d18e..91ba575 100644 --- a/src/code_index_mcp/search/grep.py +++ b/src/code_index_mcp/search/grep.py @@ -32,7 +32,8 @@ def search( context_lines: int = 0, file_pattern: Optional[str] = None, fuzzy: bool = False, - regex: bool = False + regex: bool = False, + max_line_length: Optional[int] = None ) -> Dict[str, List[Tuple[int, str]]]: """ Execute a search using standard grep. @@ -45,6 +46,7 @@ def search( file_pattern: File pattern to filter fuzzy: Enable word boundary matching regex: Enable regex pattern matching + max_line_length: Optional. Limit the length of lines when context_lines is used """ # -r: recursive, -n: line number cmd = ['grep', '-r', '-n'] @@ -102,9 +104,9 @@ def search( if process.returncode > 1: raise RuntimeError(f"grep failed with exit code {process.returncode}: {process.stderr}") - return parse_search_output(process.stdout, base_path) + return parse_search_output(process.stdout, base_path, max_line_length) except FileNotFoundError: raise RuntimeError("'grep' not found. Please install it and ensure it's in your PATH.") except Exception as e: - raise RuntimeError(f"An error occurred while running grep: {e}") + raise RuntimeError(f"An error occurred while running grep: {e}") diff --git a/src/code_index_mcp/search/ripgrep.py b/src/code_index_mcp/search/ripgrep.py index 15dd6c0..75efd3f 100644 --- a/src/code_index_mcp/search/ripgrep.py +++ b/src/code_index_mcp/search/ripgrep.py @@ -27,7 +27,8 @@ def search( context_lines: int = 0, file_pattern: Optional[str] = None, fuzzy: bool = False, - regex: bool = False + regex: bool = False, + max_line_length: Optional[int] = None ) -> Dict[str, List[Tuple[int, str]]]: """ Execute a search using ripgrep. @@ -40,6 +41,7 @@ def search( file_pattern: File pattern to filter fuzzy: Enable word boundary matching (not true fuzzy search) regex: Enable regex pattern matching + max_line_length: Optional. Limit the length of lines when context_lines is used """ cmd = ['rg', '--line-number', '--no-heading', '--color=never', '--no-ignore'] @@ -87,10 +89,10 @@ def search( if process.returncode > 1: raise RuntimeError(f"ripgrep failed with exit code {process.returncode}: {process.stderr}") - return parse_search_output(process.stdout, base_path) + return parse_search_output(process.stdout, base_path, max_line_length) except FileNotFoundError: raise RuntimeError("ripgrep (rg) not found. Please install it and ensure it's in your PATH.") except Exception as e: # Re-raise other potential exceptions like permission errors - raise RuntimeError(f"An error occurred while running ripgrep: {e}") + raise RuntimeError(f"An error occurred while running ripgrep: {e}") diff --git a/src/code_index_mcp/search/ugrep.py b/src/code_index_mcp/search/ugrep.py index 69f2cc4..9013fb5 100644 --- a/src/code_index_mcp/search/ugrep.py +++ b/src/code_index_mcp/search/ugrep.py @@ -27,7 +27,8 @@ def search( context_lines: int = 0, file_pattern: Optional[str] = None, fuzzy: bool = False, - regex: bool = False + regex: bool = False, + max_line_length: Optional[int] = None ) -> Dict[str, List[Tuple[int, str]]]: """ Execute a search using the 'ug' command-line tool. @@ -40,6 +41,7 @@ def search( file_pattern: File pattern to filter fuzzy: Enable true fuzzy search (ugrep native support) regex: Enable regex pattern matching + max_line_length: Optional. Limit the length of lines when context_lines is used """ if not self.is_available(): return {"error": "ugrep (ug) command not found."} @@ -89,7 +91,7 @@ def search( error_output = process.stderr.strip() return {"error": f"ugrep execution failed with code {process.returncode}", "details": error_output} - return parse_search_output(process.stdout, base_path) + return parse_search_output(process.stdout, base_path, max_line_length) except FileNotFoundError: return {"error": "ugrep (ug) command not found. Please ensure it's installed and in your PATH."} diff --git a/src/code_index_mcp/server.py b/src/code_index_mcp/server.py index 819fa74..243b6e9 100644 --- a/src/code_index_mcp/server.py +++ b/src/code_index_mcp/server.py @@ -122,7 +122,8 @@ def search_code_advanced( context_lines: int = 0, file_pattern: str = None, fuzzy: bool = False, - regex: bool = None + regex: bool = None, + max_line_length: int = 200 ) -> Dict[str, Any]: """ Search for a code pattern in the project using an advanced, fast tool. @@ -136,6 +137,7 @@ def search_code_advanced( context_lines: Number of lines to show before and after the match. file_pattern: A glob pattern to filter files to search in (e.g., "*.py", "*.js", "test_*.py"). + max_line_length: Optional. Default 200. Limits the length of lines when context_lines is used. All search tools now handle glob patterns consistently: - ugrep: Uses glob patterns (*.py, *.{js,ts}) - ripgrep: Uses glob patterns (*.py, *.{js,ts}) @@ -164,7 +166,8 @@ def search_code_advanced( context_lines=context_lines, file_pattern=file_pattern, fuzzy=fuzzy, - regex=regex + regex=regex, + max_line_length=max_line_length ) @mcp.tool() diff --git a/src/code_index_mcp/services/search_service.py b/src/code_index_mcp/services/search_service.py index cac3827..5351822 100644 --- a/src/code_index_mcp/services/search_service.py +++ b/src/code_index_mcp/services/search_service.py @@ -31,7 +31,8 @@ def search_code( # pylint: disable=too-many-arguments context_lines: int = 0, file_pattern: Optional[str] = None, fuzzy: bool = False, - regex: Optional[bool] = None + regex: Optional[bool] = None, + max_line_length: Optional[int] = 200 ) -> Dict[str, Any]: """ Search for code patterns in the project. @@ -45,6 +46,7 @@ def search_code( # pylint: disable=too-many-arguments file_pattern: Glob pattern to filter files fuzzy: Whether to enable fuzzy matching regex: Regex mode - True/False to force, None for auto-detection + max_line_length: Optional. Default 200. Limits the length of lines when context_lines is used. Returns: Dictionary with search results or error information @@ -89,7 +91,8 @@ def search_code( # pylint: disable=too-many-arguments context_lines=context_lines, file_pattern=file_pattern, fuzzy=fuzzy, - regex=regex + regex=regex, + max_line_length=max_line_length ) return ResponseFormatter.search_results_response(results) except Exception as e: @@ -141,4 +144,4 @@ def get_search_capabilities(self) -> Dict[str, Any]: "supports_file_patterns": True } - return capabilities \ No newline at end of file + return capabilities