diff --git a/src/gitingest/cli.py b/src/gitingest/cli.py index 81823e6..f473aa1 100644 --- a/src/gitingest/cli.py +++ b/src/gitingest/cli.py @@ -3,7 +3,7 @@ import click from gitingest.ingest import ingest -from gitingest.ingest_from_query import MAX_FILE_SIZE +from gitingest.ingest_from_query import MAX_FILE_SIZE, generate_suggestions, check_relevance, sort_suggestions from gitingest.parse_query import DEFAULT_IGNORE_PATTERNS def normalize_pattern(pattern: str) -> str: @@ -19,13 +19,28 @@ def normalize_pattern(pattern: str) -> str: @click.option('--max-size', '-s', default=MAX_FILE_SIZE, help='Maximum file size to process in bytes') @click.option('--exclude-pattern', '-e', multiple=True, help='Patterns to exclude') @click.option('--include-pattern', '-i', multiple=True, help='Patterns to include') -def main(source, output, max_size, exclude_pattern, include_pattern): +@click.option('--suggestions', is_flag=True, help='Display quick suggestions for include/exclude patterns') +def main(source, output, max_size, exclude_pattern, include_pattern, suggestions): """Analyze a directory and create a text dump of its contents.""" try: # Combine default and custom ignore patterns exclude_patterns = list(exclude_pattern) include_patterns = list(set(include_pattern)) + if suggestions: + query = { + 'local_path': source, + 'include_patterns': include_patterns, + 'ignore_patterns': exclude_patterns + } + suggestions_list = generate_suggestions(query) + relevant_suggestions = [s for s in suggestions_list if check_relevance(query, s)] + sorted_suggestions = sort_suggestions(query, relevant_suggestions) + click.echo("Quick Suggestions:") + for suggestion in sorted_suggestions[:5]: + click.echo(f"- {suggestion}") + return + if not output: output = "digest.txt" summary, tree, content = ingest(source, max_size, include_patterns, exclude_patterns, output=output) @@ -39,4 +54,4 @@ def main(source, output, max_size, exclude_pattern, include_pattern): raise click.Abort() if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/src/gitingest/ingest_from_query.py b/src/gitingest/ingest_from_query.py index 2991b09..1db7e07 100644 --- a/src/gitingest/ingest_from_query.py +++ b/src/gitingest/ingest_from_query.py @@ -339,3 +339,38 @@ def ingest_from_query(query: dict) -> Dict: else: return ingest_directory(path, query) +def generate_suggestions(query: dict) -> List[str]: + """Generate suggestions for include/exclude patterns based on repository content.""" + suggestions = [ + "*.md", "*.json", "*.py", ".gitignore", "*.docs", "*.css", "*.html", "tests/*" + ] + relevant_suggestions = [] + base_path = query['local_path'] + for suggestion in suggestions: + if should_include(base_path, base_path, [suggestion]): + relevant_suggestions.append(suggestion) + return relevant_suggestions + +def check_relevance(query: dict, suggestion: str) -> bool: + """Check the relevance of a suggestion by appending it to the current filter.""" + base_path = query['local_path'] + if should_include(base_path, base_path, [suggestion]): + return True + return False + +def sort_suggestions(query: dict, suggestions: List[str]) -> List[str]: + """Sort suggestions based on the number of files or total lines affected.""" + suggestion_stats = [] + base_path = query['local_path'] + for suggestion in suggestions: + file_count = 0 + total_lines = 0 + for root, _, files in os.walk(base_path): + for file in files: + file_path = os.path.join(root, file) + if should_include(file_path, base_path, [suggestion]): + file_count += 1 + total_lines += sum(1 for _ in open(file_path, 'r', encoding='utf-8', errors='ignore')) + suggestion_stats.append((suggestion, file_count, total_lines)) + sorted_suggestions = sorted(suggestion_stats, key=lambda x: (x[1], x[2]), reverse=True) + return [s[0] for s in sorted_suggestions] diff --git a/src/gitingest/parse_query.py b/src/gitingest/parse_query.py index 8b8f97a..c57cb2e 100644 --- a/src/gitingest/parse_query.py +++ b/src/gitingest/parse_query.py @@ -154,3 +154,11 @@ def parse_query(source: str, max_file_size: int, from_web: bool, include_pattern return query +def normalize_user_added_patterns(patterns: Union[List[str], str]) -> List[str]: + if isinstance(patterns, str): + patterns = patterns.split(',') + return [normalize_pattern(pattern) for pattern in patterns] + +def check_user_added_matches_suggestions(user_patterns: List[str], suggestions: List[str]) -> List[str]: + normalized_user_patterns = normalize_user_added_patterns(user_patterns) + return [pattern for pattern in normalized_user_patterns if pattern in suggestions] diff --git a/src/gitingest/tests/test_ingest.py b/src/gitingest/tests/test_ingest.py index 19a57b5..37fa8ce 100644 --- a/src/gitingest/tests/test_ingest.py +++ b/src/gitingest/tests/test_ingest.py @@ -2,6 +2,9 @@ from src.gitingest.ingest_from_query import ( scan_directory, extract_files_content, + generate_suggestions, + check_relevance, + sort_suggestions ) # Test fixtures @@ -100,12 +103,39 @@ def test_extract_files_content(temp_directory, sample_query): assert any('file_dir1.txt' in p for p in paths) assert any('file_dir2.txt' in p for p in paths) +def test_generate_suggestions(temp_directory, sample_query): + sample_query['local_path'] = str(temp_directory) + suggestions = generate_suggestions(sample_query) + assert "*.md" in suggestions + assert "*.py" in suggestions + assert "*.json" in suggestions + assert ".gitignore" in suggestions + assert "*.docs" in suggestions + assert "*.css" in suggestions + assert "*.html" in suggestions + assert "tests/*" in suggestions +def test_check_relevance(temp_directory, sample_query): + sample_query['local_path'] = str(temp_directory) + assert check_relevance(sample_query, "*.py") == True + assert check_relevance(sample_query, "*.md") == False + +def test_sort_suggestions(temp_directory, sample_query): + sample_query['local_path'] = str(temp_directory) + suggestions = generate_suggestions(sample_query) + sorted_suggestions = sort_suggestions(sample_query, suggestions) + assert sorted_suggestions[0] == "*.py" + assert sorted_suggestions[1] == "*.md" + assert sorted_suggestions[2] == "*.json" + assert sorted_suggestions[3] == ".gitignore" + assert sorted_suggestions[4] == "*.docs" + assert sorted_suggestions[5] == "*.css" + assert sorted_suggestions[6] == "*.html" + assert sorted_suggestions[7] == "tests/*" # TODO: test with include patterns: ['*.txt'] # TODO: test with wrong include patterns: ['*.qwerty'] - #single folder patterns # TODO: test with include patterns: ['src/*'] # TODO: test with include patterns: ['/src/*'] @@ -116,7 +146,3 @@ def test_extract_files_content(temp_directory, sample_query): # TODO: test with multiple include patterns: ['*.txt', '*.py'] # TODO: test with multiple include patterns: ['/src/*', '*.txt'] # TODO: test with multiple include patterns: ['/src*', '*.txt'] - - - - diff --git a/src/static/js/utils.js b/src/static/js/utils.js index 95b64b9..0ed9562 100644 --- a/src/static/js/utils.js +++ b/src/static/js/utils.js @@ -1,25 +1,17 @@ -// Copy functionality function copyText(className) { const textarea = document.querySelector('.' + className); const button = document.querySelector(`button[onclick="copyText('${className}')"]`); if (!textarea || !button) return; - // Copy text navigator.clipboard.writeText(textarea.value) .then(() => { - // Store original content const originalContent = button.innerHTML; - - // Change button content button.innerHTML = 'Copied!'; - - // Reset after 1 second setTimeout(() => { button.innerHTML = originalContent; }, 1000); }) .catch(err => { - // Show error in button const originalContent = button.innerHTML; button.innerHTML = 'Failed to copy'; setTimeout(() => { @@ -28,7 +20,6 @@ function copyText(className) { }); } - function handleSubmit(event, showLoading = false) { event.preventDefault(); const form = event.target || document.getElementById('ingestForm'); @@ -39,14 +30,12 @@ function handleSubmit(event, showLoading = false) { const formData = new FormData(form); - // Update file size const slider = document.getElementById('file_size'); if (slider) { formData.delete('max_file_size'); formData.append('max_file_size', slider.value); } - // Update pattern type and pattern const patternType = document.getElementById('pattern_type'); const pattern = document.getElementById('pattern'); if (patternType && pattern) { @@ -73,18 +62,14 @@ function handleSubmit(event, showLoading = false) { submitButton.classList.add('bg-[#ffb14d]'); } - // Submit the form fetch(form.action, { method: 'POST', body: formData }) .then(response => response.text()) .then(html => { - // Store the star count before updating the DOM const starCount = currentStars; - - // TEMPORARY SNOW LOGIC // const parser = new DOMParser(); const newDoc = parser.parseFromString(html, 'text/html'); @@ -93,11 +78,8 @@ function handleSubmit(event, showLoading = false) { if (existingCanvas) { document.body.insertBefore(existingCanvas, document.body.firstChild); } - // END TEMPORARY SNOW LOGIC // - // Wait for next tick to ensure DOM is updated setTimeout(() => { - // Reinitialize slider functionality initializeSlider(); const starsElement = document.getElementById('github-stars'); @@ -105,7 +87,6 @@ function handleSubmit(event, showLoading = false) { starsElement.textContent = starCount; } - // Scroll to results if they exist const resultsSection = document.querySelector('[data-results]'); if (resultsSection) { resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); @@ -141,7 +122,6 @@ function copyFullDigest() { }); } -// Add the logSliderToSize helper function function logSliderToSize(position) { const minp = 0; const maxp = 500; @@ -152,7 +132,6 @@ function logSliderToSize(position) { return Math.round(value); } -// Move slider initialization to a separate function function initializeSlider() { const slider = document.getElementById('file_size'); const sizeValue = document.getElementById('size_value'); @@ -165,14 +144,11 @@ function initializeSlider() { slider.style.backgroundSize = `${(slider.value / slider.max) * 100}% 100%`; } - // Update on slider change slider.addEventListener('input', updateSlider); - // Initialize slider position updateSlider(); } -// Add helper function for formatting size function formatSize(sizeInKB) { if (sizeInKB >= 1024) { return Math.round(sizeInKB / 1024) + 'mb'; @@ -180,17 +156,13 @@ function formatSize(sizeInKB) { return Math.round(sizeInKB) + 'kb'; } -// Initialize slider on page load document.addEventListener('DOMContentLoaded', initializeSlider); -// Make sure these are available globally window.copyText = copyText; - window.handleSubmit = handleSubmit; window.initializeSlider = initializeSlider; window.formatSize = formatSize; -// Add this new function function setupGlobalEnterHandler() { document.addEventListener('keydown', function (event) { if (event.key === 'Enter' && !event.target.matches('textarea')) { @@ -202,8 +174,38 @@ function setupGlobalEnterHandler() { }); } -// Add to the DOMContentLoaded event listener document.addEventListener('DOMContentLoaded', () => { initializeSlider(); setupGlobalEnterHandler(); }); + +function toggleSuggestion(suggestion) { + const patternInput = document.getElementById('pattern'); + if (!patternInput) return; + + const patterns = patternInput.value.split(',').map(p => p.trim()); + const index = patterns.indexOf(suggestion); + + if (index === -1) { + patterns.push(suggestion); + } else { + patterns.splice(index, 1); + } + + patternInput.value = patterns.filter(p => p).join(', '); +} + +function displaySuggestions(suggestions) { + const suggestionsContainer = document.getElementById('suggestions-container'); + if (!suggestionsContainer) return; + + suggestionsContainer.innerHTML = ''; + + suggestions.forEach(suggestion => { + const suggestionElement = document.createElement('button'); + suggestionElement.textContent = suggestion; + suggestionElement.classList.add('suggestion-button'); + suggestionElement.addEventListener('click', () => toggleSuggestion(suggestion)); + suggestionsContainer.appendChild(suggestionElement); + }); +}