ai-coding-assistant 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_coding_assistant-0.5.0.dist-info/METADATA +226 -0
- ai_coding_assistant-0.5.0.dist-info/RECORD +89 -0
- ai_coding_assistant-0.5.0.dist-info/WHEEL +4 -0
- ai_coding_assistant-0.5.0.dist-info/entry_points.txt +3 -0
- ai_coding_assistant-0.5.0.dist-info/licenses/LICENSE +21 -0
- coding_assistant/__init__.py +3 -0
- coding_assistant/__main__.py +19 -0
- coding_assistant/cli/__init__.py +1 -0
- coding_assistant/cli/app.py +158 -0
- coding_assistant/cli/commands/__init__.py +19 -0
- coding_assistant/cli/commands/ask.py +178 -0
- coding_assistant/cli/commands/config.py +438 -0
- coding_assistant/cli/commands/diagram.py +267 -0
- coding_assistant/cli/commands/document.py +410 -0
- coding_assistant/cli/commands/explain.py +192 -0
- coding_assistant/cli/commands/fix.py +249 -0
- coding_assistant/cli/commands/index.py +162 -0
- coding_assistant/cli/commands/refactor.py +245 -0
- coding_assistant/cli/commands/search.py +182 -0
- coding_assistant/cli/commands/serve_docs.py +128 -0
- coding_assistant/cli/repl.py +381 -0
- coding_assistant/cli/theme.py +90 -0
- coding_assistant/codebase/__init__.py +1 -0
- coding_assistant/codebase/crawler.py +93 -0
- coding_assistant/codebase/parser.py +266 -0
- coding_assistant/config/__init__.py +25 -0
- coding_assistant/config/config_manager.py +615 -0
- coding_assistant/config/settings.py +82 -0
- coding_assistant/context/__init__.py +19 -0
- coding_assistant/context/chunker.py +443 -0
- coding_assistant/context/enhanced_retriever.py +322 -0
- coding_assistant/context/hybrid_search.py +311 -0
- coding_assistant/context/ranker.py +355 -0
- coding_assistant/context/retriever.py +119 -0
- coding_assistant/context/window.py +362 -0
- coding_assistant/documentation/__init__.py +23 -0
- coding_assistant/documentation/agents/__init__.py +27 -0
- coding_assistant/documentation/agents/coordinator.py +510 -0
- coding_assistant/documentation/agents/module_documenter.py +111 -0
- coding_assistant/documentation/agents/synthesizer.py +139 -0
- coding_assistant/documentation/agents/task_delegator.py +100 -0
- coding_assistant/documentation/decomposition/__init__.py +21 -0
- coding_assistant/documentation/decomposition/context_preserver.py +477 -0
- coding_assistant/documentation/decomposition/module_detector.py +302 -0
- coding_assistant/documentation/decomposition/partitioner.py +621 -0
- coding_assistant/documentation/generators/__init__.py +14 -0
- coding_assistant/documentation/generators/dataflow_generator.py +440 -0
- coding_assistant/documentation/generators/diagram_generator.py +511 -0
- coding_assistant/documentation/graph/__init__.py +13 -0
- coding_assistant/documentation/graph/dependency_builder.py +468 -0
- coding_assistant/documentation/graph/module_analyzer.py +475 -0
- coding_assistant/documentation/writers/__init__.py +11 -0
- coding_assistant/documentation/writers/markdown_writer.py +322 -0
- coding_assistant/embeddings/__init__.py +0 -0
- coding_assistant/embeddings/generator.py +89 -0
- coding_assistant/embeddings/store.py +187 -0
- coding_assistant/exceptions/__init__.py +50 -0
- coding_assistant/exceptions/base.py +110 -0
- coding_assistant/exceptions/llm.py +249 -0
- coding_assistant/exceptions/recovery.py +263 -0
- coding_assistant/exceptions/storage.py +213 -0
- coding_assistant/exceptions/validation.py +230 -0
- coding_assistant/llm/__init__.py +1 -0
- coding_assistant/llm/client.py +277 -0
- coding_assistant/llm/gemini_client.py +181 -0
- coding_assistant/llm/groq_client.py +160 -0
- coding_assistant/llm/prompts.py +98 -0
- coding_assistant/llm/together_client.py +160 -0
- coding_assistant/operations/__init__.py +13 -0
- coding_assistant/operations/differ.py +369 -0
- coding_assistant/operations/generator.py +347 -0
- coding_assistant/operations/linter.py +430 -0
- coding_assistant/operations/validator.py +406 -0
- coding_assistant/storage/__init__.py +9 -0
- coding_assistant/storage/database.py +363 -0
- coding_assistant/storage/session.py +231 -0
- coding_assistant/utils/__init__.py +31 -0
- coding_assistant/utils/cache.py +477 -0
- coding_assistant/utils/hardware.py +132 -0
- coding_assistant/utils/keystore.py +206 -0
- coding_assistant/utils/logger.py +32 -0
- coding_assistant/utils/progress.py +311 -0
- coding_assistant/validation/__init__.py +13 -0
- coding_assistant/validation/files.py +305 -0
- coding_assistant/validation/inputs.py +335 -0
- coding_assistant/validation/params.py +280 -0
- coding_assistant/validation/sanitizers.py +243 -0
- coding_assistant/vcs/__init__.py +5 -0
- coding_assistant/vcs/git.py +269 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Prompt templates for the LLM."""
|
|
2
|
+
from typing import List, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PromptBuilder:
|
|
6
|
+
"""Build prompts for different tasks."""
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def build_system_prompt() -> str:
|
|
10
|
+
"""Build the system prompt for the assistant with FAANG/MAANG senior engineer capabilities."""
|
|
11
|
+
return """You are an expert AI coding assistant with the capabilities of a FAANG/MAANG Senior Software Engineer (L5/E5/SDE III level).
|
|
12
|
+
|
|
13
|
+
Your Core Capabilities:
|
|
14
|
+
|
|
15
|
+
**Architecture & System Design:**
|
|
16
|
+
- Design scalable, performant, and reliable software architectures
|
|
17
|
+
- Make informed trade-offs between different architectural approaches
|
|
18
|
+
- Address scalability, performance, security, and maintainability concerns
|
|
19
|
+
- Design distributed systems and microservices
|
|
20
|
+
- Handle real-world architectural challenges (caching, load balancing, data consistency)
|
|
21
|
+
|
|
22
|
+
**Code Quality & Best Practices:**
|
|
23
|
+
- Write clean, maintainable, production-quality code
|
|
24
|
+
- Apply SOLID principles and design patterns appropriately
|
|
25
|
+
- Optimize code for performance and efficiency
|
|
26
|
+
- Ensure code follows industry best practices and standards
|
|
27
|
+
- Implement comprehensive error handling and logging
|
|
28
|
+
|
|
29
|
+
**Code Review & Mentorship:**
|
|
30
|
+
- Conduct thorough code reviews with constructive feedback
|
|
31
|
+
- Identify bugs, security vulnerabilities, and performance issues
|
|
32
|
+
- Suggest improvements and refactoring opportunities
|
|
33
|
+
- Explain complex concepts clearly for knowledge sharing
|
|
34
|
+
- Guide junior developers toward better solutions
|
|
35
|
+
|
|
36
|
+
**Problem Solving & Algorithms:**
|
|
37
|
+
- Optimize solutions using data structures and algorithms
|
|
38
|
+
- Analyze time and space complexity
|
|
39
|
+
- Solve complex technical challenges systematically
|
|
40
|
+
- Debug issues efficiently using structured approaches
|
|
41
|
+
|
|
42
|
+
**Technical Expertise:**
|
|
43
|
+
- Deep knowledge of software development tools and processes
|
|
44
|
+
- Experience with modern tech stacks and frameworks
|
|
45
|
+
- Understanding of CI/CD, testing, and deployment pipelines
|
|
46
|
+
- Knowledge of cloud platforms and containerization
|
|
47
|
+
- Familiarity with databases, caching, and message queues
|
|
48
|
+
|
|
49
|
+
**Communication & Collaboration:**
|
|
50
|
+
- Explain technical decisions and trade-offs clearly
|
|
51
|
+
- Document code and architectural decisions
|
|
52
|
+
- Cite specific files and line numbers when referencing code
|
|
53
|
+
- Provide actionable, step-by-step guidance
|
|
54
|
+
- Ask clarifying questions when requirements are ambiguous
|
|
55
|
+
|
|
56
|
+
Guidelines:
|
|
57
|
+
- Use the provided codebase context to give accurate, context-aware answers
|
|
58
|
+
- Suggest production-ready solutions, not just quick hacks
|
|
59
|
+
- Consider long-term maintainability and team productivity
|
|
60
|
+
- Point out potential issues before they become problems
|
|
61
|
+
- If uncertain, acknowledge limitations rather than guessing
|
|
62
|
+
- Provide code examples following the project's existing patterns
|
|
63
|
+
- Think like a senior engineer: balance perfectionism with pragmatism"""
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def build_ask_prompt(question: str, file_contents: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
67
|
+
"""Build a prompt for the 'ask' command."""
|
|
68
|
+
|
|
69
|
+
system_prompt = """You are an expert coding assistant with access to the user's codebase.
|
|
70
|
+
|
|
71
|
+
Your capabilities:
|
|
72
|
+
- Explain code functionality
|
|
73
|
+
- Answer questions about the codebase
|
|
74
|
+
- Provide insights about code structure and patterns
|
|
75
|
+
|
|
76
|
+
Guidelines:
|
|
77
|
+
- Use the provided code context to give accurate answers
|
|
78
|
+
- Cite specific files when referencing code
|
|
79
|
+
- If you're not sure, say so rather than guessing
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# Build context section
|
|
83
|
+
context = "# Codebase Context:\n\n"
|
|
84
|
+
if file_contents:
|
|
85
|
+
for file_info in file_contents:
|
|
86
|
+
context += f"## File: {file_info['path']}\n"
|
|
87
|
+
context += f"```{file_info.get('language', 'text')}\n"
|
|
88
|
+
context += f"{file_info['content']}\n```\n\n"
|
|
89
|
+
else:
|
|
90
|
+
context += "No files found in the current directory.\n\n"
|
|
91
|
+
|
|
92
|
+
# User question
|
|
93
|
+
user_message = f"{context}\n# User Question:\n{question}"
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
{"role": "system", "content": system_prompt},
|
|
97
|
+
{"role": "user", "content": user_message}
|
|
98
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Together AI LLM client implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Iterator, Optional
|
|
4
|
+
import requests
|
|
5
|
+
import json
|
|
6
|
+
from coding_assistant.llm.client import BaseLLMClient
|
|
7
|
+
from coding_assistant.exceptions.llm import (
|
|
8
|
+
LLMConnectionError,
|
|
9
|
+
LLMResponseError,
|
|
10
|
+
LLMTimeoutError
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TogetherClient(BaseLLMClient):
|
|
15
|
+
"""Together AI cloud LLM client (OpenAI-compatible API)."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
api_key: Optional[str] = None,
|
|
20
|
+
model: str = "Qwen/Qwen2.5-Coder-32B-Instruct",
|
|
21
|
+
base_url: str = "https://api.together.xyz/v1"
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize Together AI client.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
api_key: Together AI API key
|
|
28
|
+
model: Model name (default: Qwen/Qwen2.5-Coder-32B-Instruct)
|
|
29
|
+
base_url: API base URL
|
|
30
|
+
"""
|
|
31
|
+
self.api_key = api_key
|
|
32
|
+
self.model = model
|
|
33
|
+
self.base_url = base_url
|
|
34
|
+
|
|
35
|
+
def generate(self, messages: List[Dict[str, str]], stream: bool = True) -> Iterator[str]:
|
|
36
|
+
"""
|
|
37
|
+
Generate response from Together AI.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
messages: List of message dicts with 'role' and 'content'
|
|
41
|
+
stream: Whether to stream the response
|
|
42
|
+
|
|
43
|
+
Yields:
|
|
44
|
+
Response chunks
|
|
45
|
+
"""
|
|
46
|
+
if not self.api_key:
|
|
47
|
+
raise LLMConnectionError(
|
|
48
|
+
provider="together",
|
|
49
|
+
endpoint=self.base_url,
|
|
50
|
+
reason="API key not set"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
headers = {
|
|
54
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
55
|
+
"Content-Type": "application/json"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
payload = {
|
|
59
|
+
"model": self.model,
|
|
60
|
+
"messages": messages,
|
|
61
|
+
"stream": stream
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
response = requests.post(
|
|
66
|
+
f"{self.base_url}/chat/completions",
|
|
67
|
+
headers=headers,
|
|
68
|
+
json=payload,
|
|
69
|
+
stream=stream,
|
|
70
|
+
timeout=120
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Check for errors
|
|
74
|
+
if response.status_code == 401 or response.status_code == 403:
|
|
75
|
+
raise LLMResponseError(
|
|
76
|
+
message="Authentication failed. Check your API key.",
|
|
77
|
+
provider="together",
|
|
78
|
+
status_code=response.status_code,
|
|
79
|
+
response_text=response.text
|
|
80
|
+
)
|
|
81
|
+
elif response.status_code == 429:
|
|
82
|
+
raise LLMResponseError(
|
|
83
|
+
message="Rate limit exceeded. Please wait and try again.",
|
|
84
|
+
provider="together",
|
|
85
|
+
status_code=429,
|
|
86
|
+
response_text=response.text
|
|
87
|
+
)
|
|
88
|
+
elif response.status_code >= 400:
|
|
89
|
+
raise LLMResponseError(
|
|
90
|
+
message=f"API error: {response.status_code}",
|
|
91
|
+
provider="together",
|
|
92
|
+
status_code=response.status_code,
|
|
93
|
+
response_text=response.text
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if stream:
|
|
97
|
+
# Parse Server-Sent Events (SSE)
|
|
98
|
+
for line in response.iter_lines():
|
|
99
|
+
if line:
|
|
100
|
+
line_str = line.decode('utf-8')
|
|
101
|
+
# SSE format: "data: {json}"
|
|
102
|
+
if line_str.startswith('data: '):
|
|
103
|
+
data_str = line_str[6:] # Remove "data: " prefix
|
|
104
|
+
|
|
105
|
+
# Check for end of stream
|
|
106
|
+
if data_str.strip() == '[DONE]':
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
chunk = json.loads(data_str)
|
|
111
|
+
# Extract content from delta
|
|
112
|
+
if 'choices' in chunk and len(chunk['choices']) > 0:
|
|
113
|
+
delta = chunk['choices'][0].get('delta', {})
|
|
114
|
+
content = delta.get('content', '')
|
|
115
|
+
if content:
|
|
116
|
+
yield content
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
continue
|
|
119
|
+
else:
|
|
120
|
+
# Non-streaming response
|
|
121
|
+
result = response.json()
|
|
122
|
+
if 'choices' in result and len(result['choices']) > 0:
|
|
123
|
+
content = result['choices'][0]['message']['content']
|
|
124
|
+
yield content
|
|
125
|
+
|
|
126
|
+
except requests.exceptions.Timeout:
|
|
127
|
+
raise LLMTimeoutError(
|
|
128
|
+
provider="together",
|
|
129
|
+
timeout_seconds=120
|
|
130
|
+
)
|
|
131
|
+
except requests.exceptions.ConnectionError as e:
|
|
132
|
+
raise LLMConnectionError(
|
|
133
|
+
provider="together",
|
|
134
|
+
endpoint=self.base_url,
|
|
135
|
+
reason=str(e)
|
|
136
|
+
)
|
|
137
|
+
except (LLMConnectionError, LLMResponseError, LLMTimeoutError):
|
|
138
|
+
# Re-raise our custom exceptions
|
|
139
|
+
raise
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise LLMConnectionError(
|
|
142
|
+
provider="together",
|
|
143
|
+
endpoint=self.base_url,
|
|
144
|
+
reason=f"Unexpected error: {str(e)}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def is_available(self) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Check if Together AI is available.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if API key is set
|
|
153
|
+
"""
|
|
154
|
+
# Basic check: API key is set
|
|
155
|
+
if not self.api_key:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# Could optionally validate with API call here
|
|
159
|
+
# but keeping it simple for now to avoid unnecessary API calls
|
|
160
|
+
return True
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Code operations module for generation, validation, and modification."""
|
|
2
|
+
|
|
3
|
+
from coding_assistant.operations.generator import CodeGenerator
|
|
4
|
+
from coding_assistant.operations.validator import SyntaxValidator
|
|
5
|
+
from coding_assistant.operations.differ import DiffGenerator
|
|
6
|
+
from coding_assistant.operations.linter import LinterIntegration
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'CodeGenerator',
|
|
10
|
+
'SyntaxValidator',
|
|
11
|
+
'DiffGenerator',
|
|
12
|
+
'LinterIntegration',
|
|
13
|
+
]
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Generate and display code diffs."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from typing import List, Optional, Tuple
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DiffGenerator:
|
|
14
|
+
"""Generate and display code differences."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize the diff generator."""
|
|
18
|
+
self.console = Console()
|
|
19
|
+
|
|
20
|
+
def generate_diff(self, original: str, modified: str,
|
|
21
|
+
filepath: Optional[str] = None,
|
|
22
|
+
context_lines: int = 3) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Generate a unified diff between two code versions.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
original: The original code
|
|
28
|
+
modified: The modified code
|
|
29
|
+
filepath: Optional file path for diff headers
|
|
30
|
+
context_lines: Number of context lines to include
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Unified diff as a string
|
|
34
|
+
"""
|
|
35
|
+
original_lines = original.splitlines(keepends=True)
|
|
36
|
+
modified_lines = modified.splitlines(keepends=True)
|
|
37
|
+
|
|
38
|
+
filename = filepath or 'file'
|
|
39
|
+
|
|
40
|
+
diff = difflib.unified_diff(
|
|
41
|
+
original_lines,
|
|
42
|
+
modified_lines,
|
|
43
|
+
fromfile=f"a/{filename}",
|
|
44
|
+
tofile=f"b/{filename}",
|
|
45
|
+
lineterm='',
|
|
46
|
+
n=context_lines
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return ''.join(diff)
|
|
50
|
+
|
|
51
|
+
def generate_side_by_side_diff(self, original: str, modified: str) -> List[Tuple[str, str, str]]:
|
|
52
|
+
"""
|
|
53
|
+
Generate a side-by-side diff.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
original: The original code
|
|
57
|
+
modified: The modified code
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of (status, original_line, modified_line) tuples
|
|
61
|
+
status can be: 'unchanged', 'modified', 'added', 'removed'
|
|
62
|
+
"""
|
|
63
|
+
original_lines = original.splitlines()
|
|
64
|
+
modified_lines = modified.splitlines()
|
|
65
|
+
|
|
66
|
+
matcher = difflib.SequenceMatcher(None, original_lines, modified_lines)
|
|
67
|
+
result = []
|
|
68
|
+
|
|
69
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
70
|
+
if tag == 'equal':
|
|
71
|
+
for i in range(i1, i2):
|
|
72
|
+
result.append(('unchanged', original_lines[i], modified_lines[j1 + (i - i1)]))
|
|
73
|
+
elif tag == 'replace':
|
|
74
|
+
# Lines were modified
|
|
75
|
+
for i in range(i1, i2):
|
|
76
|
+
old_line = original_lines[i] if i < len(original_lines) else ''
|
|
77
|
+
new_line = modified_lines[j1 + (i - i1)] if (j1 + (i - i1)) < len(modified_lines) else ''
|
|
78
|
+
result.append(('modified', old_line, new_line))
|
|
79
|
+
elif tag == 'delete':
|
|
80
|
+
for i in range(i1, i2):
|
|
81
|
+
result.append(('removed', original_lines[i], ''))
|
|
82
|
+
elif tag == 'insert':
|
|
83
|
+
for j in range(j1, j2):
|
|
84
|
+
result.append(('added', '', modified_lines[j]))
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def display_diff(self, diff_text: str, language: str = 'diff',
|
|
89
|
+
title: Optional[str] = None):
|
|
90
|
+
"""
|
|
91
|
+
Display a unified diff in the terminal with syntax highlighting.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
diff_text: The unified diff text
|
|
95
|
+
language: Language for syntax highlighting (default: diff)
|
|
96
|
+
title: Optional title for the diff panel
|
|
97
|
+
"""
|
|
98
|
+
if not diff_text.strip():
|
|
99
|
+
self.console.print("[yellow]No changes detected[/yellow]")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
syntax = Syntax(
|
|
103
|
+
diff_text,
|
|
104
|
+
'diff',
|
|
105
|
+
theme='monokai',
|
|
106
|
+
line_numbers=False,
|
|
107
|
+
word_wrap=False
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if title:
|
|
111
|
+
self.console.print(Panel(syntax, title=title, border_style="blue"))
|
|
112
|
+
else:
|
|
113
|
+
self.console.print(syntax)
|
|
114
|
+
|
|
115
|
+
def display_side_by_side(self, original: str, modified: str,
|
|
116
|
+
title: Optional[str] = None,
|
|
117
|
+
language: str = 'python'):
|
|
118
|
+
"""
|
|
119
|
+
Display a side-by-side diff with color coding.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
original: The original code
|
|
123
|
+
modified: The modified code
|
|
124
|
+
title: Optional title
|
|
125
|
+
language: Programming language for context
|
|
126
|
+
"""
|
|
127
|
+
diff_lines = self.generate_side_by_side_diff(original, modified)
|
|
128
|
+
|
|
129
|
+
table = Table(
|
|
130
|
+
title=title,
|
|
131
|
+
show_header=True,
|
|
132
|
+
header_style="bold magenta",
|
|
133
|
+
border_style="blue",
|
|
134
|
+
show_lines=True
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
table.add_column("", width=3) # Status indicator
|
|
138
|
+
table.add_column("Line", width=5)
|
|
139
|
+
table.add_column("Original", style="dim")
|
|
140
|
+
table.add_column("Modified", style="dim")
|
|
141
|
+
|
|
142
|
+
line_num = 1
|
|
143
|
+
for status, old_line, new_line in diff_lines:
|
|
144
|
+
if status == 'unchanged':
|
|
145
|
+
icon = " "
|
|
146
|
+
style = "dim"
|
|
147
|
+
elif status == 'modified':
|
|
148
|
+
icon = "~"
|
|
149
|
+
style = "yellow"
|
|
150
|
+
elif status == 'added':
|
|
151
|
+
icon = "+"
|
|
152
|
+
style = "green"
|
|
153
|
+
elif status == 'removed':
|
|
154
|
+
icon = "-"
|
|
155
|
+
style = "red"
|
|
156
|
+
|
|
157
|
+
table.add_row(
|
|
158
|
+
f"[{style}]{icon}[/{style}]",
|
|
159
|
+
str(line_num),
|
|
160
|
+
old_line[:80], # Truncate long lines
|
|
161
|
+
new_line[:80],
|
|
162
|
+
style=style
|
|
163
|
+
)
|
|
164
|
+
line_num += 1
|
|
165
|
+
|
|
166
|
+
self.console.print(table)
|
|
167
|
+
|
|
168
|
+
def display_summary(self, diff_text: str):
|
|
169
|
+
"""
|
|
170
|
+
Display a summary of changes.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
diff_text: The unified diff text
|
|
174
|
+
"""
|
|
175
|
+
lines = diff_text.split('\n')
|
|
176
|
+
|
|
177
|
+
added = sum(1 for line in lines if line.startswith('+') and not line.startswith('+++'))
|
|
178
|
+
removed = sum(1 for line in lines if line.startswith('-') and not line.startswith('---'))
|
|
179
|
+
modified = min(added, removed)
|
|
180
|
+
net_added = added - modified
|
|
181
|
+
net_removed = removed - modified
|
|
182
|
+
|
|
183
|
+
summary = Text()
|
|
184
|
+
summary.append("Changes: ", style="bold")
|
|
185
|
+
|
|
186
|
+
if modified > 0:
|
|
187
|
+
summary.append(f"{modified} modified ", style="yellow")
|
|
188
|
+
if net_added > 0:
|
|
189
|
+
summary.append(f"{net_added} added ", style="green")
|
|
190
|
+
if net_removed > 0:
|
|
191
|
+
summary.append(f"{net_removed} removed ", style="red")
|
|
192
|
+
|
|
193
|
+
if not any([modified, net_added, net_removed]):
|
|
194
|
+
summary.append("No changes", style="dim")
|
|
195
|
+
|
|
196
|
+
self.console.print(summary)
|
|
197
|
+
|
|
198
|
+
def generate_patch(self, original: str, modified: str,
|
|
199
|
+
filepath: str) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Generate a patch file that can be applied with `patch` command.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
original: The original code
|
|
205
|
+
modified: The modified code
|
|
206
|
+
filepath: The file path for the patch
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Patch content as string
|
|
210
|
+
"""
|
|
211
|
+
return self.generate_diff(original, modified, filepath, context_lines=3)
|
|
212
|
+
|
|
213
|
+
def apply_patch(self, original: str, patch: str) -> Optional[str]:
|
|
214
|
+
"""
|
|
215
|
+
Apply a patch to original content.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
original: The original code
|
|
219
|
+
patch: The unified diff patch
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Modified content if successful, None otherwise
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
original_lines = original.splitlines(keepends=True)
|
|
226
|
+
patch_lines = patch.splitlines(keepends=True)
|
|
227
|
+
|
|
228
|
+
# Use difflib to apply patch
|
|
229
|
+
# This is a simple implementation
|
|
230
|
+
# For production, consider using a proper patch library
|
|
231
|
+
|
|
232
|
+
result_lines = original_lines.copy()
|
|
233
|
+
|
|
234
|
+
# Parse patch and apply
|
|
235
|
+
# This is simplified - full patch parsing is complex
|
|
236
|
+
# For now, just return None to indicate manual application needed
|
|
237
|
+
|
|
238
|
+
return None # TODO: Implement proper patch application
|
|
239
|
+
|
|
240
|
+
except Exception:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def compare_files(self, file1_path: str, file2_path: str,
|
|
244
|
+
display: bool = True) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Compare two files and generate a diff.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
file1_path: Path to first file
|
|
250
|
+
file2_path: Path to second file
|
|
251
|
+
display: Whether to display the diff
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Unified diff as string
|
|
255
|
+
"""
|
|
256
|
+
path1 = Path(file1_path)
|
|
257
|
+
path2 = Path(file2_path)
|
|
258
|
+
|
|
259
|
+
if not path1.exists():
|
|
260
|
+
raise FileNotFoundError(f"File not found: {file1_path}")
|
|
261
|
+
if not path2.exists():
|
|
262
|
+
raise FileNotFoundError(f"File not found: {file2_path}")
|
|
263
|
+
|
|
264
|
+
content1 = path1.read_text(encoding='utf-8')
|
|
265
|
+
content2 = path2.read_text(encoding='utf-8')
|
|
266
|
+
|
|
267
|
+
diff = self.generate_diff(content1, content2, file1_path)
|
|
268
|
+
|
|
269
|
+
if display:
|
|
270
|
+
self.display_diff(diff, title=f"{file1_path} vs {file2_path}")
|
|
271
|
+
|
|
272
|
+
return diff
|
|
273
|
+
|
|
274
|
+
def get_change_stats(self, diff_text: str) -> dict:
|
|
275
|
+
"""
|
|
276
|
+
Get statistics about changes in a diff.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
diff_text: The unified diff text
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dict with stats: lines_added, lines_removed, files_changed, etc.
|
|
283
|
+
"""
|
|
284
|
+
lines = diff_text.split('\n')
|
|
285
|
+
|
|
286
|
+
stats = {
|
|
287
|
+
'lines_added': 0,
|
|
288
|
+
'lines_removed': 0,
|
|
289
|
+
'lines_modified': 0,
|
|
290
|
+
'files_changed': 0,
|
|
291
|
+
'hunks': 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for line in lines:
|
|
295
|
+
if line.startswith('+++'):
|
|
296
|
+
stats['files_changed'] += 1
|
|
297
|
+
elif line.startswith('@@'):
|
|
298
|
+
stats['hunks'] += 1
|
|
299
|
+
elif line.startswith('+') and not line.startswith('+++'):
|
|
300
|
+
stats['lines_added'] += 1
|
|
301
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
302
|
+
stats['lines_removed'] += 1
|
|
303
|
+
|
|
304
|
+
stats['lines_modified'] = min(stats['lines_added'], stats['lines_removed'])
|
|
305
|
+
stats['net_change'] = stats['lines_added'] - stats['lines_removed']
|
|
306
|
+
|
|
307
|
+
return stats
|
|
308
|
+
|
|
309
|
+
def highlight_changes(self, original: str, modified: str,
|
|
310
|
+
language: str = 'python') -> Tuple[str, str]:
|
|
311
|
+
"""
|
|
312
|
+
Highlight specific character-level changes between lines.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
original: Original code line
|
|
316
|
+
modified: Modified code line
|
|
317
|
+
language: Programming language
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Tuple of (highlighted_original, highlighted_modified)
|
|
321
|
+
"""
|
|
322
|
+
# Use SequenceMatcher for character-level diff
|
|
323
|
+
matcher = difflib.SequenceMatcher(None, original, modified)
|
|
324
|
+
|
|
325
|
+
orig_highlighted = []
|
|
326
|
+
mod_highlighted = []
|
|
327
|
+
|
|
328
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
329
|
+
if tag == 'equal':
|
|
330
|
+
orig_highlighted.append(original[i1:i2])
|
|
331
|
+
mod_highlighted.append(modified[j1:j2])
|
|
332
|
+
elif tag == 'replace':
|
|
333
|
+
orig_highlighted.append(f"[red]{original[i1:i2]}[/red]")
|
|
334
|
+
mod_highlighted.append(f"[green]{modified[j1:j2]}[/green]")
|
|
335
|
+
elif tag == 'delete':
|
|
336
|
+
orig_highlighted.append(f"[red]{original[i1:i2]}[/red]")
|
|
337
|
+
elif tag == 'insert':
|
|
338
|
+
mod_highlighted.append(f"[green]{modified[j1:j2]}[/green]")
|
|
339
|
+
|
|
340
|
+
return ''.join(orig_highlighted), ''.join(mod_highlighted)
|
|
341
|
+
|
|
342
|
+
def format_diff_for_display(self, diff_text: str) -> str:
|
|
343
|
+
"""
|
|
344
|
+
Format diff text for better terminal display.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
diff_text: Raw unified diff
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Formatted diff with colors
|
|
351
|
+
"""
|
|
352
|
+
lines = diff_text.split('\n')
|
|
353
|
+
formatted = []
|
|
354
|
+
|
|
355
|
+
for line in lines:
|
|
356
|
+
if line.startswith('+++'):
|
|
357
|
+
formatted.append(f"[blue]{line}[/blue]")
|
|
358
|
+
elif line.startswith('---'):
|
|
359
|
+
formatted.append(f"[blue]{line}[/blue]")
|
|
360
|
+
elif line.startswith('@@'):
|
|
361
|
+
formatted.append(f"[cyan]{line}[/cyan]")
|
|
362
|
+
elif line.startswith('+'):
|
|
363
|
+
formatted.append(f"[green]{line}[/green]")
|
|
364
|
+
elif line.startswith('-'):
|
|
365
|
+
formatted.append(f"[red]{line}[/red]")
|
|
366
|
+
else:
|
|
367
|
+
formatted.append(line)
|
|
368
|
+
|
|
369
|
+
return '\n'.join(formatted)
|