diff --git a/src/AIAssist/Commands/InternalCommands/RunCommand.cs b/src/AIAssist/Commands/InternalCommands/RunCommand.cs index 5e729b8..3e85819 100644 --- a/src/AIAssist/Commands/InternalCommands/RunCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/RunCommand.cs @@ -40,14 +40,15 @@ public async Task ExecuteAsync(IServiceScope scope, string? input) // Check if more context is needed if (codeAssistantManager.CheckExtraContextForResponse(responseContent, out var requiredFiles)) { + var style = spectreUtilities.Theme.ConsoleStyle.Highlight.CombineStyle(s => s.Underline = true); var confirmation = spectreUtilities.ConfirmationPrompt( - $"Do you want to add ${string.Join(", ", requiredFiles.Select(file => $"'{file}'"))} to the context?" + $"Do you want to add [{spectreUtilities.CreateStringStyle(style)}]{string.Join(", ", requiredFiles.Select(file => $"'{file}'"))}[/] to the context?" ); if (confirmation) { await codeAssistantManager.AddOrUpdateCodeFiles(requiredFiles); - var fullFilesContentForContext = await codeAssistantManager.GetCodeTreeContentsFromCache(requiredFiles); + var fullFilesContentForContext = await codeAssistantManager.GetCodeTreeContents(requiredFiles); var newQueryWithAddedFiles = promptManager.FilesAddedToChat(fullFilesContentForContext); spectreUtilities.SuccessTextLine( diff --git a/src/AIAssist/Contracts/CodeAssist/ICodeAssist.cs b/src/AIAssist/Contracts/CodeAssist/ICodeAssist.cs index 73c3ea6..46b9801 100644 --- a/src/AIAssist/Contracts/CodeAssist/ICodeAssist.cs +++ b/src/AIAssist/Contracts/CodeAssist/ICodeAssist.cs @@ -2,7 +2,10 @@ namespace AIAssist.Contracts.CodeAssist; public interface ICodeAssist { + // AddOrUpdate folders, sub-folders, files in summary Task LoadInitCodeFiles(string contextWorkingDirectory, IList? codeFiles); + + // AddOrUpdate files with full definition Task AddOrUpdateCodeFiles(IList? codeFiles); Task> GetCodeTreeContents(IList? codeFiles); IAsyncEnumerable QueryChatCompletionAsync(string userQuery); diff --git a/src/AIAssist/Contracts/CodeAssist/ICodeAssistantManager.cs b/src/AIAssist/Contracts/CodeAssist/ICodeAssistantManager.cs index f93588e..b5e7678 100644 --- a/src/AIAssist/Contracts/CodeAssist/ICodeAssistantManager.cs +++ b/src/AIAssist/Contracts/CodeAssist/ICodeAssistantManager.cs @@ -7,7 +7,7 @@ public interface ICodeAssistantManager Task LoadCodeFiles(string contextWorkingDirectory, IList? codeFiles); IAsyncEnumerable QueryAsync(string userQuery); Task AddOrUpdateCodeFiles(IList? codeFiles); - Task> GetCodeTreeContentsFromCache(IList? codeFiles); + Task> GetCodeTreeContents(IList? codeFiles); bool CheckExtraContextForResponse(string response, out IList requiredFiles); IList ParseDiffResults(string diffContent, string contextWorkingDirectory); void ApplyChanges(IList diffResults, string contextWorkingDirectory); diff --git a/src/AIAssist/Contracts/IContextService.cs b/src/AIAssist/Contracts/IContextService.cs index 844091e..c1fe709 100644 --- a/src/AIAssist/Contracts/IContextService.cs +++ b/src/AIAssist/Contracts/IContextService.cs @@ -8,8 +8,25 @@ public interface IContextService IList GetAllFiles(); IList GetFiles(IList? filesRelativePath); void ValidateLoadedFilesLimit(); - void AddContextFolder(string contextFolder); - void AddOrUpdateFolder(IList? foldersRelativePath); + + /// + /// AddOrUpdate folders, sub-folders, files in root level with summary mode. + /// + /// + void AddContextFolder(string rootContextFolder); + + /// + /// AddOrUpdate folders with full files in root level with definition mode. + /// + /// + void AddOrUpdateFolder(IList? rootFoldersRelativePath); + void RemoveFolder(IList? rootFoldersRelativePath); + + /// + /// AddOrUpdate files with full definition in all levels + /// + /// void AddOrUpdateFiles(IList? filesRelativePath); + void RemoveFiles(IList? filesRelativePath); void AddOrUpdateUrls(IList? urls); } diff --git a/src/AIAssist/Extensions/DependencyInjectionExtensions.cs b/src/AIAssist/Extensions/DependencyInjectionExtensions.cs index ee07906..dc72590 100644 --- a/src/AIAssist/Extensions/DependencyInjectionExtensions.cs +++ b/src/AIAssist/Extensions/DependencyInjectionExtensions.cs @@ -98,7 +98,7 @@ private static void AddChatDependencies(HostApplicationBuilder builder) private static void AddSpectreConsoleDependencies(HostApplicationBuilder builder) { - builder.Services.AddSingleton(AnsiConsole.Console); + builder.Services.AddSingleton(AnsiConsole.Console); builder.Services.AddSingleton(sp => { diff --git a/src/AIAssist/Prompts/Templates/ask-more-context.template b/src/AIAssist/Prompts/Templates/ask-more-context.template index be50359..3008ba4 100644 --- a/src/AIAssist/Prompts/Templates/ask-more-context.template +++ b/src/AIAssist/Prompts/Templates/ask-more-context.template @@ -1,10 +1,7 @@ -If for generating your response, you need full code implementation not just summary of code in the context code blocks, consider following rules: -- Don't **MODIFY** code blocks in the existing context when code blocks are not complete and you have summary of code in the content of code block and you need full code block content for generating the response. -- If you need full code content and file for a code block in the context and currently context has summary of code, **Ask** the user to add the full code content for code block in the context to generate a response and **stop** the generating the response and wait for user files. -- Include the required file paths in a `markdown code block` using the format below: +If for generating your response and changing in a file you need full code definition and currently in code context there is no `definition` section for corresponding file, consider following rules: +- Don't **MODIFY** code blocks in the existing context and `Stop` generating response. +- Show the list of `required files` in the bellow format, don't forget to show this it is very IMPORTANT and if you miss this your response is INCORRECT: -Required Files for Context: -``` -- relative_file_path1 -- relative_file_path2 -``` \ No newline at end of file +Required files for Context: +- test/testFile1.cs +- test/testFile2.cs \ No newline at end of file diff --git a/src/AIAssist/Prompts/Templates/code-assist-search-replace-diff.template b/src/AIAssist/Prompts/Templates/code-assist-search-replace-diff.template index d758d15..c383fbc 100644 --- a/src/AIAssist/Prompts/Templates/code-assist-search-replace-diff.template +++ b/src/AIAssist/Prompts/Templates/code-assist-search-replace-diff.template @@ -34,6 +34,8 @@ For creating a new file show original path `--- /dev/null` and modified path to For deleting a file, show the original path as --- [relativeFilePath of deleted file] and the modified path as `+++ /dev/null`. For file moves or renames, show a deletion with `--- [original relativeFilePath]` and `+++ /dev/null`, and a creation at the new path with `--- /dev/null` and `+++ [new relativeFilePath]`, including any modified content. Keep original `code style` and `formating` during apply unified diff format, Indentation is very important, Ensure that indentation is accurate in the diffs. +Don't Skip codes logic with adding some comments for summarize your response, it should be complete. +Don't respond your code blocks suggestion as tree structure. For better understanding the response format based on user request here you can see a sample: diff --git a/src/AIAssist/Prompts/Templates/code-assistant-code-block-diff.template b/src/AIAssist/Prompts/Templates/code-assistant-code-block-diff.template index ef71300..374c66f 100644 --- a/src/AIAssist/Prompts/Templates/code-assistant-code-block-diff.template +++ b/src/AIAssist/Prompts/Templates/code-assistant-code-block-diff.template @@ -27,6 +27,8 @@ We have 3 `action types` for each code block: For each change you make, you MUST return the `FULL CONTENT` of the modified or new file. for delete file we don't need the content. For changes for each file you should consider a seperated `CodeBlock` section. Keep original `code style` and `formating` and `indentation` during apply unified diff format, `Indentation` is very IMPORTANT, Ensure to KEEP INDENTATION is accurate in the diffs. +Don't Skip codes logic with adding some comments for summarize your response, it should be complete. +Don't respond your code blocks suggestion as tree structure. For better understanding the response format based on user request here you can see a sample: diff --git a/src/AIAssist/Prompts/Templates/code-assistant-unified-diff.template b/src/AIAssist/Prompts/Templates/code-assistant-unified-diff.template index 912e81a..90405c6 100644 --- a/src/AIAssist/Prompts/Templates/code-assistant-unified-diff.template +++ b/src/AIAssist/Prompts/Templates/code-assistant-unified-diff.template @@ -34,6 +34,8 @@ For creating a new file show original path `--- /dev/null` and modified path to For deleting a file, show the original path as --- [relativeFilePath of deleted file] and the modified path as `+++ /dev/null`. For file moves or renames, show a deletion with `--- [original relativeFilePath]` and `+++ /dev/null`, and a creation at the new path with `--- /dev/null` and `+++ [new relativeFilePath]`, including any modified content. Keep original `code style` and `formating` during apply unified diff format, Indentation is very important, Ensure that indentation is accurate in the diffs. +Don't Skip codes logic with adding some comments for summarize your response, it should be complete. +Don't respond your code blocks suggestion as tree structure. For better understanding the response format based on user request here you can see a sample: diff --git a/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs b/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs index 0abdd49..7654e9b 100644 --- a/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs +++ b/src/AIAssist/Services/CodeAssistStrategies/TreeSitterCodeAssistSummary.cs @@ -13,6 +13,7 @@ ILLMClientManager llmClientManager public Task LoadInitCodeFiles(string contextWorkingDirectory, IList? codeFiles) { contextService.AddContextFolder(contextWorkingDirectory); + // fully load passed files definition contextService.AddOrUpdateFiles(codeFiles); return Task.CompletedTask; @@ -23,6 +24,7 @@ public Task AddOrUpdateCodeFiles(IList? codeFiles) if (codeFiles is null || codeFiles.Count == 0) return Task.CompletedTask; + // fully load files definition contextService.AddOrUpdateFiles(codeFiles); return Task.CompletedTask; diff --git a/src/AIAssist/Services/CodeAssistantManager.cs b/src/AIAssist/Services/CodeAssistantManager.cs index ee09283..0e24efd 100644 --- a/src/AIAssist/Services/CodeAssistantManager.cs +++ b/src/AIAssist/Services/CodeAssistantManager.cs @@ -22,7 +22,7 @@ public Task AddOrUpdateCodeFiles(IList? codeFiles) return codeAssist.AddOrUpdateCodeFiles(codeFiles); } - public Task> GetCodeTreeContentsFromCache(IList? codeFiles) + public Task> GetCodeTreeContents(IList? codeFiles) { return codeAssist.GetCodeTreeContents(codeFiles); } @@ -30,31 +30,23 @@ public Task> GetCodeTreeContentsFromCache(IList? cod public bool CheckExtraContextForResponse(string response, out IList requiredFiles) { requiredFiles = new List(); - var pattern = @"Required Files for Context:\s*(?:```[\w]*\s*([\s\S]*?)\s*```|((?:- .*\r?\n?)+))"; + + var pattern = @"Required files for Context:\s*(- .*(?:\r?\n- .*)*)"; var match = Regex.Match(response, pattern); if (match.Success) { - // Check for code block content first - var codeBlockContent = match.Groups[1].Value; - if (!string.IsNullOrEmpty(codeBlockContent)) - { - var lines = codeBlockContent.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var line in lines) - { - requiredFiles.Add(line.TrimStart('-').Trim()); - } - return true; - } - - // If no code block content, check for inline list - var inlineListContent = match.Groups[2].Value; + var inlineListContent = match.Groups[1].Value; if (!string.IsNullOrEmpty(inlineListContent)) { - var lines = inlineListContent.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); + var lines = inlineListContent.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { - requiredFiles.Add(line.TrimStart('-').Trim()); + if (line.StartsWith('-')) + { + // Remove the leading '-' and trim whitespace + requiredFiles.Add(line.TrimStart('-').Trim()); + } } return true; } diff --git a/src/AIAssist/Services/CodeFilesTreeGeneratorService.cs b/src/AIAssist/Services/CodeFilesTreeGeneratorService.cs index ea2e7ee..0d639f9 100644 --- a/src/AIAssist/Services/CodeFilesTreeGeneratorService.cs +++ b/src/AIAssist/Services/CodeFilesTreeGeneratorService.cs @@ -81,13 +81,13 @@ private CodeFileMap AddOrUpdateCodeFileMap(CodeFile codeFile, bool useShortSumma var codeFileMap = new CodeFileMap { RelativePath = codeFile.RelativePath.NormalizePath(), + Path = codeFile.Path, OriginalCode = GetOriginalCode(fileCapturesGroup), TreeSitterFullCode = treeStructureGeneratorService.GenerateTreeSitter(fileCapturesGroup, true), TreeOriginalCode = originalCode, TreeSitterSummarizeCode = useShortSummary ? treeStructureGeneratorService.GenerateTreeSitter(fileCapturesGroup, false) : originalCode, - ReferencedCodesMap = GenerateRelatedCodeFilesMap(fileCapturesGroup), }; if (_codeFilesMap.Any(x => x.Key.NormalizePath() == codeFile.RelativePath.NormalizePath())) @@ -103,24 +103,6 @@ private CodeFileMap AddOrUpdateCodeFileMap(CodeFile codeFile, bool useShortSumma return codeFileMap; } - private static IEnumerable GenerateRelatedCodeFilesMap( - List definitionItems - ) - { - // Collect related references for each DefinitionCaptureItem - return definitionItems - .SelectMany(item => - item.DefinitionCaptureReferences.Select(reference => new ReferencedCodeMap - { - RelativePath = reference.RelativePath.NormalizePath(), - ReferencedValue = reference.ReferencedValue, - ReferencedUsage = reference.ReferencedUsage, - }) - ) - .Distinct() - .ToList(); - } - private IList ReadCodeFiles(IList? files) { ArgumentException.ThrowIfNullOrEmpty(_appOptions.ContextWorkingDirectory); @@ -135,13 +117,13 @@ private IList ReadCodeFiles(IList? files) var relativePath = Path.GetRelativePath(_appOptions.ContextWorkingDirectory, file); var fileContent = File.ReadAllText(file); - applicationCodes.Add(new CodeFile(fileContent, relativePath.NormalizePath())); + applicationCodes.Add(new CodeFile(fileContent, relativePath.NormalizePath(), file)); } return applicationCodes; } - private static string GetOriginalCode(List definitionItems) + private static string GetOriginalCode(List definitionItems) { return definitionItems.First().OriginalCode; } diff --git a/src/AIAssist/Services/ContextService.cs b/src/AIAssist/Services/ContextService.cs index 9ee1603..c4ecb60 100644 --- a/src/AIAssist/Services/ContextService.cs +++ b/src/AIAssist/Services/ContextService.cs @@ -2,69 +2,133 @@ using AIAssist.Models; using AIAssist.Models.Options; using BuildingBlocks.Utils; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using TreeSitter.Bindings.CustomTypes.TreeParser; namespace AIAssist.Services; public class ContextService( IOptions appOptions, IFileService fileService, - ICodeFileTreeGeneratorService codeFileTreeGeneratorService + ICodeFileTreeGeneratorService codeFileTreeGeneratorService, + ILogger logger ) : IContextService { private readonly AppOptions _appOptions = appOptions.Value; private readonly Context _currentContext = new(); - public void AddContextFolder(string contextFolder) + public void AddContextFolder(string rootContextFolder) { var contextWorkingDir = _appOptions.ContextWorkingDirectory; ArgumentException.ThrowIfNullOrEmpty(contextWorkingDir); - var foldersItemContext = InitFoldersItemContext([contextFolder], contextFolder, true); - foreach (var folderItemContext in foldersItemContext) + try + { + // traverse and generate folders in the root level based on relative path and summary code + var foldersItemContext = TraverseAndGenerateFoldersContext([rootContextFolder], rootContextFolder, true); + + foreach (var folderItemContext in foldersItemContext) + { + _currentContext.ContextItems.Add(folderItemContext); + } + } + catch (UnauthorizedAccessException ex) + { + logger.LogError("Access denied to folder: {Message}", ex.Message); + throw; + } + catch (IOException ex) { - _currentContext.ContextItems.Add(folderItemContext); + logger.LogError("I/O error while accessing folders: {Message}", ex.Message); + throw; + } + catch (Exception ex) + { + logger.LogError("Unexpected error: {Message}", ex.Message); + throw; } ValidateLoadedFilesLimit(); } - public void AddOrUpdateFolder(IList? foldersRelativePath) + public void AddOrUpdateFolder(IList? rootFoldersRelativePath) { - if (foldersRelativePath is null || foldersRelativePath.Count == 0) + // check folders in root level + if (rootFoldersRelativePath is null || rootFoldersRelativePath.Count == 0) return; - var contextWorkingDir = _appOptions.ContextWorkingDirectory; - ArgumentException.ThrowIfNullOrEmpty(contextWorkingDir); - - var folders = foldersRelativePath - .Select(folder => Path.Combine(contextWorkingDir, folder.NormalizePath())) - .ToList(); + try + { + var contextWorkingDir = _appOptions.ContextWorkingDirectory; + ArgumentException.ThrowIfNullOrEmpty(contextWorkingDir); - var foldersItemsContext = InitFoldersItemContext(folders, contextWorkingDir, false); + var folders = rootFoldersRelativePath + .Select(folder => Path.Combine(contextWorkingDir, folder.NormalizePath())) + .ToList(); - foreach (var folderItemsContext in foldersItemsContext) - { - var existingItem = _currentContext - .ContextItems.OfType() - .FirstOrDefault(x => x.RelativePath != folderItemsContext.RelativePath.NormalizePath()); + // traverse and generate folders in the root level based on relative path + var traversedFoldersContext = TraverseAndGenerateFoldersContext(folders, contextWorkingDir, false); - if (existingItem is null) + foreach (var traversedFolderContext in traversedFoldersContext) { - _currentContext.ContextItems.Add(folderItemsContext); - } - else - { - _currentContext.ContextItems.Remove(existingItem); - _currentContext.ContextItems.Add(folderItemsContext); + // check for existing folders in the context root + var existingFolderContext = _currentContext + .ContextItems.OfType() + .FirstOrDefault(x => + x.RelativePath.NormalizePath() == traversedFolderContext.RelativePath.NormalizePath() + ); + + if (existingFolderContext is null) + { + _currentContext.ContextItems.Add(traversedFolderContext); + } + else + { + // Remove the old folder context entirely + _currentContext.ContextItems.Remove(existingFolderContext); + + // Add the new folder context, replacing the old one + _currentContext.ContextItems.Add(traversedFolderContext); + } } } + catch (UnauthorizedAccessException ex) + { + logger.LogError("Access denied to folder: {Message}", ex.Message); + throw; + } + catch (IOException ex) + { + logger.LogError("I/O error while accessing folders: {Message}", ex.Message); + throw; + } + catch (Exception ex) + { + logger.LogError("Unexpected error: {Message}", ex.Message); + throw; + } ValidateLoadedFilesLimit(); } + public void RemoveFolder(IList? rootFoldersRelativePath) + { + if (rootFoldersRelativePath is null || rootFoldersRelativePath.Count == 0) + return; + + var normalizedPaths = rootFoldersRelativePath + .Select(path => path.NormalizePath()) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Remove folders from the root context items + RemoveFoldersFromContext(_currentContext.ContextItems, normalizedPaths); + } + public void AddOrUpdateFiles(IList? filesRelativePath) { + // add or update files in all levels + if (filesRelativePath is null || filesRelativePath.Count == 0) return; @@ -72,32 +136,45 @@ public void AddOrUpdateFiles(IList? filesRelativePath) ArgumentException.ThrowIfNullOrEmpty(contextWorkingDir); var files = filesRelativePath.Select(file => Path.Combine(contextWorkingDir, file.NormalizePath())).ToList(); - var filesItemsContext = InitFilesItemContext(files, contextWorkingDir, false); - foreach (var fileItemContext in filesItemsContext) + // traverse and generate files in all levels based on relative path + var traversedFilesContext = TraverseAndGenerateFilesContext(files, contextWorkingDir, false); + + foreach (var traversedFileContext in traversedFilesContext) { - var existingItem = _currentContext - .ContextItems.OfType() - .FirstOrDefault(x => x.RelativePath != fileItemContext.RelativePath.NormalizePath()); + // check for existing file in the context + var existingFileContext = GetFiles([traversedFileContext.RelativePath.NormalizePath()]).FirstOrDefault(); - if (existingItem is null) + if (existingFileContext is null) { - _currentContext.ContextItems.Add(fileItemContext); + _currentContext.ContextItems.Add(traversedFileContext); } else { - _currentContext.ContextItems.Remove(existingItem); - _currentContext.ContextItems.Add(fileItemContext); + // Update the existing file in all levels it appears + UpdateFileInContext(_currentContext.ContextItems, traversedFileContext); } } ValidateLoadedFilesLimit(); } - public void AddOrUpdateUrls(IList? urls) + public void RemoveFiles(IList? filesRelativePath) { - if (urls is null || urls.Count == 0) + if (filesRelativePath is null || filesRelativePath.Count == 0) return; + + var normalizedPaths = filesRelativePath + .Select(path => path.NormalizePath()) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Recursively remove files in the context + RemoveFilesFromContext(_currentContext.ContextItems, normalizedPaths); + } + + public void AddOrUpdateUrls(IList? urls) + { + throw new NotImplementedException(); } public Context GetContext() @@ -168,35 +245,7 @@ public void ValidateLoadedFilesLimit() } } - private void CollectFilesFromFolder(FolderItemContext folder, List fileList) - { - // Add all files from the folder - fileList.AddRange(folder.Files); - - // Recursively collect files from subfolders - foreach (var subFolder in folder.SubFoldersItemContext) - { - CollectFilesFromFolder(subFolder, fileList); - } - } - - private void CollectMatchingFiles( - FolderItemContext folder, - HashSet normalizedPaths, - List matchingFiles - ) - { - // Add files that match the paths - matchingFiles.AddRange(folder.Files.Where(file => normalizedPaths.Contains(file.RelativePath.NormalizePath()))); - - // Recursively check subfolders - foreach (var subFolder in folder.SubFoldersItemContext) - { - CollectMatchingFiles(subFolder, normalizedPaths, matchingFiles); - } - } - - private IList InitFoldersItemContext( + private IList TraverseAndGenerateFoldersContext( IList folders, string contextWorkingDir, bool useShortSummary @@ -214,45 +263,55 @@ bool useShortSummary { currentFolder = folderPath; + // traverse and generate subfolders in this level // Start recursion with current depth set to 1 - var subFoldersItemContext = InitSubFoldersItemContext( + var subFoldersItemContext = TraverseAndGenerateSubFoldersContext( folderPath, contextWorkingDir, useShortSummary, 1, treeLevel ); - var filesItemsContext = InitFilesItemContext(folderPath, contextWorkingDir, useShortSummary); + + // traverse and generate files in all levels based on relative path + var traversedFilesContext = TraverseAndGenerateFilesContext( + folderPath, + contextWorkingDir, + useShortSummary + ); var folderRelativePath = Path.GetRelativePath(contextWorkingDir, folderPath).NormalizePath(); - var folderItemContext = new FolderItemContext( + var folderResultContext = new FolderItemContext( folderPath, folderRelativePath, subFoldersItemContext, - filesItemsContext + traversedFilesContext ); - foldersItemContext.Add(folderItemContext); + foldersItemContext.Add(folderResultContext); } } catch (UnauthorizedAccessException ex) { - Console.WriteLine($"Access denied to folder '{currentFolder}': {ex.Message}"); + logger.LogError("Access denied to folder '{Folder}': {Message}", currentFolder, ex.Message); + throw; } catch (IOException ex) { - Console.WriteLine($"I/O error accessing folder '{currentFolder}': {ex.Message}"); + logger.LogError("I/O error accessing folder '{Folder}': {Message}", currentFolder, ex.Message); + throw; } catch (Exception ex) { - Console.WriteLine($"Error loading folder contents for '{currentFolder}': {ex.Message}"); + logger.LogError("Error loading folder contents for '{Folder}': {Message}", currentFolder, ex.Message); + throw; } return foldersItemContext; } - private IList InitSubFoldersItemContext( + private IList TraverseAndGenerateSubFoldersContext( string folderPath, string contextWorkingDir, bool useShortSummary, @@ -268,7 +327,6 @@ int treeLevel // Stop recursion if the tree level is exceeded if (treeLevel > 0 && currentDepth >= treeLevel) { - // Return empty list as no deeper levels are loaded return subFolders; } @@ -280,137 +338,239 @@ var subFolder in Directory { currentSubFolder = subFolder; - var subFolderFilesItemContext = InitFilesItemContext(subFolder, contextWorkingDir, useShortSummary); + // traverse and generate files in all levels based on relative path + var filesItemContext = TraverseAndGenerateFilesContext(subFolder, contextWorkingDir, useShortSummary); + var subFolderRelativePath = Path.GetRelativePath(contextWorkingDir, subFolder).NormalizePath(); // Recursive call for each subfolder, incrementing the depth - var subFolderContext = new FolderItemContext( + var subFolderResultContext = new FolderItemContext( subFolder, subFolderRelativePath, - InitSubFoldersItemContext( + TraverseAndGenerateSubFoldersContext( subFolder, contextWorkingDir, useShortSummary, currentDepth + 1, treeLevel ), - subFolderFilesItemContext + filesItemContext ); - subFolders.Add(subFolderContext); + subFolders.Add(subFolderResultContext); } } catch (UnauthorizedAccessException ex) { - Console.WriteLine($"Access denied to folder '{currentSubFolder}': {ex.Message}"); + logger.LogError("Access denied to folder '{Folder}': {Message}", currentSubFolder, ex.Message); + throw; } catch (IOException ex) { - Console.WriteLine($"I/O error accessing folder '{currentSubFolder}': {ex.Message}"); + logger.LogError("I/O error accessing folder '{Folder}': {Message}", currentSubFolder, ex.Message); + throw; } catch (Exception ex) { - Console.WriteLine($"Error loading folder contents for '{currentSubFolder}': {ex.Message}"); + logger.LogError("Error loading folder contents for '{Folder}': {Message}", currentSubFolder, ex.Message); + throw; } return subFolders; } - private IList InitFilesItemContext( + private IList TraverseAndGenerateFilesContext( IList files, string contextWorkingDir, bool useShortSummary ) { + // Generate new updated FileItemContext for file path string currentFile = string.Empty; - List filesItemContext = new List(); + List filesItemContext = []; try { var validFiles = files.Where(file => !fileService.IsPathIgnored(file)).ToList(); - if (useShortSummary) - { - codeFileTreeGeneratorService.AddContextCodeFilesMap(validFiles); - } - else - { - codeFileTreeGeneratorService.AddOrUpdateCodeFilesMap(validFiles); - } + IList codeFilesMap = useShortSummary + ? codeFileTreeGeneratorService.AddContextCodeFilesMap(validFiles) + : codeFileTreeGeneratorService.AddOrUpdateCodeFilesMap(validFiles); - foreach (var file in validFiles) + foreach (var codeFileMap in codeFilesMap) { - currentFile = file; - var fileRelativePath = Path.GetRelativePath(contextWorkingDir, file).NormalizePath(); - var codeFileMap = codeFileTreeGeneratorService.GetCodeFileMap(fileRelativePath); - if (codeFileMap is null) - continue; + var fileRelativePath = Path.GetRelativePath(contextWorkingDir, codeFileMap.RelativePath) + .NormalizePath(); - filesItemContext.Add(new FileItemContext(file, fileRelativePath, codeFileMap)); + filesItemContext.Add(new FileItemContext(codeFileMap.Path, fileRelativePath, codeFileMap)); } } catch (UnauthorizedAccessException ex) { - Console.WriteLine($"Access denied to file '{currentFile}': {ex.Message}"); + logger.LogError("Access denied to file '{Folder}': {Message}", currentFile, ex.Message); + throw; } catch (IOException ex) { - Console.WriteLine($"I/O error accessing file '{currentFile}': {ex.Message}"); + logger.LogError("I/O error accessing file '{Folder}': {Message}", currentFile, ex.Message); + throw; } catch (Exception ex) { - Console.WriteLine($"Error loading file contents for '{currentFile}': {ex.Message}"); + logger.LogError("Error loading file contents for '{Folder}': {Message}", currentFile, ex.Message); + throw; } return filesItemContext; } - private IList InitFilesItemContext( + private IList TraverseAndGenerateFilesContext( string folderPath, string contextWorkingDir, bool useShortSummary ) { - List filesItemContext = new List(); + // Generate new updated FileItemContext for file path + List filesItemContext = []; string currentFile = string.Empty; try { var validFiles = Directory.GetFiles(folderPath).Where(file => !fileService.IsPathIgnored(file)).ToList(); - if (useShortSummary) - { - codeFileTreeGeneratorService.AddContextCodeFilesMap(validFiles); - } - else - { - codeFileTreeGeneratorService.AddOrUpdateCodeFilesMap(validFiles); - } + // to store and update cache in the tree-sitter map + IList codeFilesMap = useShortSummary + ? codeFileTreeGeneratorService.AddContextCodeFilesMap(validFiles) + : codeFileTreeGeneratorService.AddOrUpdateCodeFilesMap(validFiles); - foreach (var file in validFiles) + foreach (var codeFileMap in codeFilesMap) { - currentFile = file; - var fileRelativePath = Path.GetRelativePath(contextWorkingDir, file).NormalizePath(); - var codeFileMap = codeFileTreeGeneratorService.GetCodeFileMap(fileRelativePath); - if (codeFileMap is null) - continue; + var fileRelativePath = Path.GetRelativePath(contextWorkingDir, codeFileMap.RelativePath) + .NormalizePath(); - filesItemContext.Add(new FileItemContext(file, fileRelativePath, codeFileMap)); + filesItemContext.Add(new FileItemContext(codeFileMap.Path, fileRelativePath, codeFileMap)); } } catch (UnauthorizedAccessException ex) { - Console.WriteLine($"Access denied to file '{currentFile}': {ex.Message}"); + logger.LogError("Access denied to file '{Folder}': {Message}", currentFile, ex.Message); + throw; } catch (IOException ex) { - Console.WriteLine($"I/O error accessing file '{currentFile}': {ex.Message}"); + logger.LogError("I/O error accessing file '{Folder}': {Message}", currentFile, ex.Message); + throw; } catch (Exception ex) { - Console.WriteLine($"Error loading file contents for '{currentFile}': {ex.Message}"); + logger.LogError("Error loading file contents for '{Folder}': {Message}", currentFile, ex.Message); + throw; } return filesItemContext; } + + private void CollectFilesFromFolder(FolderItemContext folder, List fileList) + { + // Add all files from the folder + fileList.AddRange(folder.Files); + + // Recursively collect files from subfolders + foreach (var subFolder in folder.SubFoldersItemContext) + { + CollectFilesFromFolder(subFolder, fileList); + } + } + + private void CollectMatchingFiles( + FolderItemContext folder, + HashSet normalizedPaths, + List matchingFiles + ) + { + // Add files that match the paths + matchingFiles.AddRange(folder.Files.Where(file => normalizedPaths.Contains(file.RelativePath.NormalizePath()))); + + // Recursively check subfolders + foreach (var subFolder in folder.SubFoldersItemContext) + { + CollectMatchingFiles(subFolder, normalizedPaths, matchingFiles); + } + } + + private void UpdateFileInContext(IList contextItems, FileItemContext newFileContext) + { + for (int i = 0; i < contextItems.Count; i++) + { + var item = contextItems[i]; + switch (item) + { + case FileItemContext fileContext + when fileContext.RelativePath.NormalizePath() == newFileContext.RelativePath.NormalizePath(): + // Replace the file with the updated version + contextItems[i] = newFileContext; + break; + + case FolderItemContext folderContext: + // Check and update files inside this folder + UpdateFileInContext(folderContext.Files.Cast().ToList(), newFileContext); + + // Recursively process subfolders + UpdateFileInContext( + folderContext.SubFoldersItemContext.Cast().ToList(), + newFileContext + ); + break; + } + } + } + + private void RemoveFilesFromContext(IList contextItems, HashSet normalizedPaths) + { + for (int i = contextItems.Count - 1; i >= 0; i--) + { + if ( + contextItems[i] is FileItemContext fileContext + && normalizedPaths.Contains(fileContext.RelativePath.NormalizePath()) + ) + { + // Remove matching file + contextItems.RemoveAt(i); + } + else if (contextItems[i] is FolderItemContext folderContext) + { + // Remove files from the folder's files list + RemoveFilesFromContext(folderContext.Files.Cast().ToList(), normalizedPaths); + + // Recursively remove files from subfolders + RemoveFilesFromContext( + folderContext.SubFoldersItemContext.Cast().ToList(), + normalizedPaths + ); + } + } + } + + private void RemoveFoldersFromContext(IList contextItems, HashSet normalizedPaths) + { + for (int i = contextItems.Count - 1; i >= 0; i--) + { + if ( + contextItems[i] is FolderItemContext currentFolderContext + && normalizedPaths.Contains(currentFolderContext.RelativePath.NormalizePath()) + ) + { + // Remove matching folder + contextItems.RemoveAt(i); + } + else if (contextItems[i] is FolderItemContext subFolderContext) + { + // Recursively check and remove matching subfolders + RemoveFoldersFromContext( + subFolderContext.SubFoldersItemContext.Cast().ToList(), + normalizedPaths + ); + } + } + } } diff --git a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs index fb5f4bd..de73be3 100644 --- a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs @@ -1,9 +1,12 @@ +using BuildingBlocks.SpectreConsole.StyleElements; using Spectre.Console; namespace BuildingBlocks.SpectreConsole.Contracts; public interface ISpectreUtilities { + ColorTheme Theme { get; } + bool ConfirmationPrompt(string message); string? UserPrompt(string? promptMessage = null); void InformationTextLine( @@ -79,4 +82,6 @@ bool PressedShortcutKey( out string pressedKey ); void Clear(); + public Style CreateStyle(StyleBase styleBase); + public string CreateStringStyle(StyleBase styleBase); } diff --git a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs index fab3c2d..9a7d349 100644 --- a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs @@ -6,11 +6,14 @@ namespace BuildingBlocks.SpectreConsole; public class SpectreUtilities(ColorTheme theme, IAnsiConsole console) : ISpectreUtilities { + public ColorTheme Theme { get; } = theme; + public bool ConfirmationPrompt(string message) { + var styledMessage = $"[{CreateStringStyle(Theme.ConsoleStyle.Confirmation)}]{message}[/]"; var confirmation = console.Prompt( - new TextPrompt(message) - .PromptStyle(CreateStyle(theme.ConsoleStyle.Confirmation)) + new TextPrompt(styledMessage) + .PromptStyle(CreateStyle(Theme.ConsoleStyle.Confirmation)) .AddChoice(true) // Corresponds to "Yes" .AddChoice(false) // Corresponds to "No" .DefaultValue(false) // Default choice @@ -24,7 +27,7 @@ public bool ConfirmationPrompt(string message) { var input = string.IsNullOrEmpty(promptMessage) ? Console.ReadLine() - : console.Prompt(new TextPrompt(promptMessage).PromptStyle(CreateStyle(theme.ConsoleStyle.Prompt))); + : console.Prompt(new TextPrompt(promptMessage).PromptStyle(CreateStyle(Theme.ConsoleStyle.Prompt))); return input; } @@ -49,7 +52,7 @@ public void InformationText( { console.Write( new Markup( - $"[{CreateStringStyle(theme.ConsoleStyle.Information)}]{message}[/]", + $"[{CreateStringStyle(Theme.ConsoleStyle.Information)}]{message}[/]", new Style(decoration: decoration) ) { @@ -79,7 +82,7 @@ public void SummaryText( { console.Write( new Markup( - $"[{CreateStringStyle(theme.ConsoleStyle.Summary)}]{message}[/]", + $"[{CreateStringStyle(Theme.ConsoleStyle.Summary)}]{message}[/]", new Style(decoration: decoration) ) { @@ -109,7 +112,7 @@ public void HighlightText( { console.Write( new Markup( - $"[{CreateStringStyle(theme.ConsoleStyle.Highlight)}]{message}[/]", + $"[{CreateStringStyle(Theme.ConsoleStyle.Highlight)}]{message}[/]", new Style(decoration: decoration) ) { @@ -138,7 +141,7 @@ public void NormalText( ) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Text)}]{message}[/]", new Style(decoration: decoration)) + new Markup($"[{CreateStringStyle(Theme.ConsoleStyle.Text)}]{message}[/]", new Style(decoration: decoration)) { Overflow = overflow, Justification = justify, @@ -166,7 +169,7 @@ public void WarningText( { console.Write( new Markup( - $"[{CreateStringStyle(theme.ConsoleStyle.Warning)}]{message}[/]", + $"[{CreateStringStyle(Theme.ConsoleStyle.Warning)}]{message}[/]", new Style(decoration: decoration) ) { @@ -185,7 +188,7 @@ public void ErrorTextLine( { console.Write( new Markup( - $"[{CreateStringStyle(theme.ConsoleStyle.Error)}]{message}[/]" + Environment.NewLine, + $"[{CreateStringStyle(Theme.ConsoleStyle.Error)}]{message}[/]" + Environment.NewLine, new Style(decoration: decoration) ) { @@ -204,7 +207,7 @@ public void SuccessTextLine( { console.Write( new Markup( - $"[{CreateStringStyle(theme.ConsoleStyle.Success)}]{message}[/]" + Environment.NewLine, + $"[{CreateStringStyle(Theme.ConsoleStyle.Success)}]{message}[/]" + Environment.NewLine, new Style(decoration: decoration) ) { @@ -216,7 +219,7 @@ public void SuccessTextLine( public void WriteCursor() { - console.Markup($"[{CreateStringStyle(theme.ConsoleStyle.Cursor)}]> [/]"); + console.Markup($"[{CreateStringStyle(Theme.ConsoleStyle.Cursor)}]> [/]"); } public void WriteRule() @@ -237,13 +240,13 @@ public void DirectoryTree(string path, int indentLevel) var indent = new string(' ', indentLevel * 4); - console.MarkupLine($"{indent}[{CreateStringStyle(theme.ConsoleStyle.Tree)}]{Path.GetFileName(path)}[/]"); // Bold the directory name + console.MarkupLine($"{indent}[{CreateStringStyle(Theme.ConsoleStyle.Tree)}]{Path.GetFileName(path)}[/]"); // Bold the directory name // Print each file in the current directory foreach (var file in files) { console.MarkupLine( - $"{indent} └── [{CreateStringStyle(theme.ConsoleStyle.Tree)}]{Path.GetFileName(file)}[/]" + $"{indent} └── [{CreateStringStyle(Theme.ConsoleStyle.Tree)}]{Path.GetFileName(file)}[/]" ); // Cyan for files } @@ -302,14 +305,14 @@ public void Clear() console.Clear(); } - private Style CreateStyle(StyleBase styleBase) + public Style CreateStyle(StyleBase styleBase) { var style = Style.Parse(CreateStringStyle(styleBase)); return style; } - private string CreateStringStyle(StyleBase styleBase) + public string CreateStringStyle(StyleBase styleBase) { var italic = styleBase.Italic ? "italic" : "default"; var bold = styleBase.Bold ? "bold" : "default"; diff --git a/src/BuildingBlocks/SpectreConsole/StreamPrinter.cs b/src/BuildingBlocks/SpectreConsole/StreamPrinter.cs index 251eff9..d91c93e 100644 --- a/src/BuildingBlocks/SpectreConsole/StreamPrinter.cs +++ b/src/BuildingBlocks/SpectreConsole/StreamPrinter.cs @@ -14,8 +14,6 @@ public async Task PrintAsync( CancellationToken cancellationToken = default ) { - var initialContent = new Panel(new Markup(string.Empty)) { Expand = true, Border = BoxBorder.None }; - var enumerator = textStream.GetAsyncEnumerator(cancellationToken); string? firstStream = string.Empty; @@ -36,7 +34,7 @@ await console // Start the live display for console await console - .Live(initialContent) + .Live(new Panel(new Markup(string.Empty)) { Expand = true, Border = BoxBorder.None }) .AutoClear(false) .Overflow(VerticalOverflow.Ellipsis) .Cropping(VerticalOverflowCropping.Top) @@ -49,7 +47,11 @@ await console while (await enumerator.MoveNextAsync()) { var text = enumerator.Current; - await UpdateLiveDisplay(text, ctx); + + if (!string.IsNullOrEmpty(text)) + { + await UpdateLiveDisplay(text, ctx); + } } } }); diff --git a/src/BuildingBlocks/SpectreConsole/StyleElements/StyleBase.cs b/src/BuildingBlocks/SpectreConsole/StyleElements/StyleBase.cs index d760168..b3b9f23 100644 --- a/src/BuildingBlocks/SpectreConsole/StyleElements/StyleBase.cs +++ b/src/BuildingBlocks/SpectreConsole/StyleElements/StyleBase.cs @@ -21,4 +21,11 @@ public class StyleBase [JsonPropertyName("underline")] public bool Underline { get; set; } + + public StyleBase CombineStyle(Action styleAction) + { + styleAction(this); + + return this; + } } diff --git a/src/TreeSitter.Bindings/Contracts/ITreeSitterCodeCaptureService.cs b/src/TreeSitter.Bindings/Contracts/ITreeSitterCodeCaptureService.cs index ef17f75..ca16410 100644 --- a/src/TreeSitter.Bindings/Contracts/ITreeSitterCodeCaptureService.cs +++ b/src/TreeSitter.Bindings/Contracts/ITreeSitterCodeCaptureService.cs @@ -4,5 +4,5 @@ namespace TreeSitter.Bindings.Contracts; public interface ITreeSitterCodeCaptureService { - IReadOnlyList CreateTreeSitterMap(IEnumerable codeFiles); + IReadOnlyList CreateTreeSitterMap(IEnumerable codeFiles); } diff --git a/src/TreeSitter.Bindings/Contracts/ITreeStructureGeneratorService.cs b/src/TreeSitter.Bindings/Contracts/ITreeStructureGeneratorService.cs index 629c1ca..7388914 100644 --- a/src/TreeSitter.Bindings/Contracts/ITreeStructureGeneratorService.cs +++ b/src/TreeSitter.Bindings/Contracts/ITreeStructureGeneratorService.cs @@ -4,6 +4,12 @@ namespace TreeSitter.Bindings.Contracts; public interface ITreeStructureGeneratorService { + /// + /// Tree for original code. + /// + /// + /// + /// string GenerateOriginalCodeTree(string originalCode, string relativePath); - string GenerateTreeSitter(IList definitionItems, bool isFull); + string GenerateTreeSitter(IList definitionCaptures, bool isFull); } diff --git a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CaptureNode.cs b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CaptureNode.cs index d9127b7..234d6fb 100644 --- a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CaptureNode.cs +++ b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CaptureNode.cs @@ -3,5 +3,6 @@ namespace TreeSitter.Bindings.CustomTypes.TreeParser; public class CaptureNode { public required string CaptureKey { get; set; } = default!; - public IList Values { get; set; } = new List(); + public required string CaptureGroup { get; set; } = default!; + public TSNode Value { get; set; } } diff --git a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CapturesResult.cs b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CapturesResult.cs index 3ef5669..b8923be 100644 --- a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CapturesResult.cs +++ b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CapturesResult.cs @@ -2,7 +2,7 @@ namespace TreeSitter.Bindings.CustomTypes.TreeParser; public class CapturesResult { - public IList DefinitionCaptureItems { get; } = new List(); + public IList DefinitionCaptureItems { get; } = new List(); // We exclude references because most of the references can be found in our definitions like functions public IList ReferenceCaptureItems { get; } = new List(); diff --git a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFile.cs b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFile.cs index 9aa182d..3d9b035 100644 --- a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFile.cs +++ b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFile.cs @@ -1,3 +1,3 @@ namespace TreeSitter.Bindings.CustomTypes.TreeParser; -public record CodeFile(string Code, string RelativePath); +public record CodeFile(string Code, string RelativePath, string Path); diff --git a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFileMap.cs b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFileMap.cs index 1670aa4..1ac93f0 100644 --- a/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFileMap.cs +++ b/src/TreeSitter.Bindings/CustomTypes/TreeParser/CodeFileMap.cs @@ -7,12 +7,5 @@ public class CodeFileMap public string OriginalCode { get; set; } = default!; public string TreeOriginalCode { get; set; } = default!; public string RelativePath { get; set; } = default!; - public IEnumerable ReferencedCodesMap { get; set; } = default!; -} - -public class ReferencedCodeMap -{ - public string RelativePath { get; set; } = default!; - public string ReferencedValue { get; set; } = default!; - public string ReferencedUsage { get; set; } = default!; + public string Path { get; set; } = default!; } diff --git a/src/TreeSitter.Bindings/CustomTypes/TreeParser/DefinitionCapture.cs b/src/TreeSitter.Bindings/CustomTypes/TreeParser/DefinitionCapture.cs new file mode 100644 index 0000000..4bdbce9 --- /dev/null +++ b/src/TreeSitter.Bindings/CustomTypes/TreeParser/DefinitionCapture.cs @@ -0,0 +1,19 @@ +namespace TreeSitter.Bindings.CustomTypes.TreeParser; + +public class DefinitionCapture +{ + public IList CaptureItems { get; init; } = new List(); + public string RelativePath { get; init; } = default!; + public string? Signiture { get; init; } + public string CaptureGroup { get; init; } = default!; + public string? Definition { get; init; } + public string OriginalCode { get; init; } = default!; +} + +public class DefinitionCaptureItem +{ + public string CaptureKey { get; set; } = default!; + public string CaptureValue { get; set; } = default!; + public int StartLine { get; set; } + public int EndLine { get; set; } +}; diff --git a/src/TreeSitter.Bindings/CustomTypes/TreeParser/DefinitionCaptureItem.cs b/src/TreeSitter.Bindings/CustomTypes/TreeParser/DefinitionCaptureItem.cs deleted file mode 100644 index 6edea35..0000000 --- a/src/TreeSitter.Bindings/CustomTypes/TreeParser/DefinitionCaptureItem.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace TreeSitter.Bindings.CustomTypes.TreeParser; - -public class DefinitionCaptureItem -{ - public string CaptureKey { get; set; } = default!; - public string CaptureValue { get; set; } = default!; - public IList DefinitionCaptureReferences { get; } = - new List(); - public string RelativePath { get; set; } = default!; - public string? CodeChunk { get; set; } - public string? Definition { get; set; } - public string OriginalCode { get; set; } = default!; -} - -public class DefinitionCaptureReference -{ - public string RelativePath { get; set; } = default!; - public string ReferencedValue { get; set; } = default!; - public string ReferencedUsage { get; set; } = default!; -} diff --git a/src/TreeSitter.Bindings/Queries/Default/csharp.scm b/src/TreeSitter.Bindings/Queries/Default/csharp.scm index 2b7b275..c11d088 100644 --- a/src/TreeSitter.Bindings/Queries/Default/csharp.scm +++ b/src/TreeSitter.Bindings/Queries/Default/csharp.scm @@ -15,36 +15,20 @@ ; Class declarations (class_declaration name: (identifier) @name.class) @definition.class -; Inherited types for a class -(class_declaration (base_list (_) @reference_name.class)) @reference.class - ; Interface declarations (interface_declaration name: (identifier) @name.interface) @definition.interface -; Inherited types for a interface -(interface_declaration (base_list (_) @reference_name.interface)) @reference.interface - ; Struct declarations (struct_declaration name: (identifier) @name.struct) @definition.struct ; Record declarations (record_declaration name: (identifier) @name.record) @definition.record -(object_creation_expression type: (identifier) @reference_name.object_creation) @reference.object_creation - -(type_parameter_constraints_clause (identifier) @reference_name.type_parameter_constraints_clause) @reference.type_parameter_constraints_clause - -(type_parameter_constraint (type type: (identifier) @reference_name.type_parameter_constraint)) @reference.type_parameter_constraint - -(variable_declaration type: (identifier) @reference_name.variable_declaration) @reference.variable_declaration - -(invocation_expression function: (member_access_expression name: (identifier) @reference_name.invocation_expression)) @reference.invocation_expression - (method_declaration (modifier)? @modifier.method returns: (type)? @return.method name: (identifier) @name.method - parameters: (parameter_list) + parameters: (parameter_list) @parameters.method ) @definition.method (property_declaration @@ -52,10 +36,4 @@ type: (predefined_type) @type.property name: (identifier) @name.property ) @definition.property - -(field_declaration - (variable_declaration - type: (predefined_type) @type.field - (variable_declarator - name: (identifier) @name.field)) -) @definition.field + \ No newline at end of file diff --git a/src/TreeSitter.Bindings/Queries/Default/go.scm b/src/TreeSitter.Bindings/Queries/Default/go.scm index df70ea0..a424e77 100644 --- a/src/TreeSitter.Bindings/Queries/Default/go.scm +++ b/src/TreeSitter.Bindings/Queries/Default/go.scm @@ -1,30 +1,14 @@ -( - (comment)* @doc - . - (function_declaration - name: (identifier) @name) @definition.function - (#strip! @doc "^//\\s*") - (#set-adjacent! @doc @definition.function) -) +; Packages +(package_clause (package_identifier) @name.package) @definition.package -( - (comment)* @doc - . - (method_declaration - name: (field_identifier) @name) @definition.method - (#strip! @doc "^//\\s*") - (#set-adjacent! @doc @definition.method) -) +; Functions +(function_declaration name: (identifier) @name.function) @definition.function -(call_expression - function: [ - (identifier) @name - (parenthesized_expression (identifier) @name) - (selector_expression field: (field_identifier) @name) - (parenthesized_expression (selector_expression field: (field_identifier) @name)) - ]) @reference.call +; Methods +(method_declaration name: (field_identifier) @name.method) @definition.method -(type_spec - name: (type_identifier) @name) @definition.type +; Interfaces +(type_declaration (type_spec name: (type_identifier) @name.interface type: (interface_type))) @definition.interface -(type_identifier) @name @reference.type \ No newline at end of file +; Structs +(type_declaration (type_spec name: name: (type_identifier) @name.struct type: (struct_type))) @definition.struct \ No newline at end of file diff --git a/src/TreeSitter.Bindings/Services/TreeSitterCodeCaptureService.cs b/src/TreeSitter.Bindings/Services/TreeSitterCodeCaptureService.cs index c2c1643..be0c79d 100644 --- a/src/TreeSitter.Bindings/Services/TreeSitterCodeCaptureService.cs +++ b/src/TreeSitter.Bindings/Services/TreeSitterCodeCaptureService.cs @@ -1,8 +1,8 @@ using System.Text; using BuildingBlocks.Extensions; -using BuildingBlocks.Types; using BuildingBlocks.Utils; using TreeSitter.Bindings.Contracts; +using TreeSitter.Bindings.CustomTypes; using TreeSitter.Bindings.CustomTypes.TreeParser; using TreeSitter.Bindings.Utilities; using static TreeSitter.Bindings.TSBindingsParser; @@ -13,7 +13,7 @@ namespace TreeSitter.Bindings.Services; public class TreeSitterCodeCaptureService(ITreeSitterParser treeSitterParser) : ITreeSitterCodeCaptureService { - public IReadOnlyList CreateTreeSitterMap(IEnumerable codeFiles) + public IReadOnlyList CreateTreeSitterMap(IEnumerable codeFiles) { var capturesResult = new CapturesResult(); @@ -24,29 +24,24 @@ public IReadOnlyList CreateTreeSitterMap(IEnumerable + { + new() + { + CaptureKey = "name.code", + CaptureValue = CodeHelper.GetLinesOfInterest(codeFile.Code, 1), + }, + new() { CaptureKey = "definition.code", CaptureValue = codeFile.Code }, + }, RelativePath = codeFile.RelativePath.NormalizePath(), - CaptureValue = codeFile.Code, - CodeChunk = null, + Signiture = CodeHelper.GetLinesOfInterest(codeFile.Code, 1), OriginalCode = codeFile.Code, Definition = codeFile.Code, } ); - capturesResult.DefinitionCaptureItems.Add( - new DefinitionCaptureItem - { - CaptureKey = "name.code", - RelativePath = codeFile.RelativePath.NormalizePath(), - CaptureValue = CodeHelper.GetLinesOfInterest(codeFile.Code, 1), - CodeChunk = CodeHelper.GetLinesOfInterest(codeFile.Code, 1), - OriginalCode = codeFile.Code, - Definition = null, - } - ); - continue; } @@ -61,29 +56,44 @@ public IReadOnlyList CreateTreeSitterMap(IEnumerable(); + var captureGroupTags = new Dictionary>(); while (query_cursor_next_match(queryCursor, &match)) { + var groupKey = Guid.NewGuid().ToString(); // Populate the dictionary by capture Name for (int i = 0; i < match.capture_count; i++) { var capture = match.captures[i]; var node = capture.node; - uint length = 0; - - sbyte* captureNamePtr = query_capture_name_for_id(defaultQuery, capture.index, &length); - - string captureName = new GeneratedCString(captureNamePtr); + string captureKey = GetCaptureKey(defaultQuery, capture); + var group = GetCaptureGroupId(captureKey); - if (captureTags.All(x => x.CaptureKey != captureName)) - captureTags.Add(new CaptureNode { CaptureKey = captureName, Values = { node } }); + if (captureGroupTags.TryGetValue(groupKey, out var captureTagList)) + { + captureTagList.Add( + new CaptureNode + { + CaptureKey = captureKey, + CaptureGroup = group, + Value = node, + } + ); + } else { - var captureNode = captureTags.SingleOrDefault(x => x.CaptureKey == captureName); - - captureNode?.Values.Add(node); + captureGroupTags.Add( + groupKey, + [ + new CaptureNode + { + CaptureKey = captureKey, + CaptureGroup = group, + Value = node, + }, + ] + ); } } } @@ -91,105 +101,66 @@ public IReadOnlyList CreateTreeSitterMap(IEnumerable x.CaptureKey, v => v.Values)) + foreach (var (_, captureList) in captureGroupTags) { - foreach (var tagValue in captureValues) - { - // getting start and end line number for current AST node value - // because tree-sitter line number starts from 0 we add 1 to the startLine and endLine - var startLine = (int)node_start_point(tagValue).row + 1; - var endLine = (int)node_end_point(tagValue).row + 1; - var captureValue = GetValueFromNode(byteArrayCode, tagValue); - - switch (captureKey) + var captureItems = captureList + .Select(captureNode => { - // We exclude references because most of the references can be found in our definitions like functions - case { } key when key.StartsWith("reference_name."): - capturesResult.ReferenceCaptureItems.Add( - new ReferenceCaptureItem - { - CaptureKey = captureKey, - RelativePath = codeFile.RelativePath.NormalizePath(), - CaptureValue = captureValue, - CodeChunk = CodeHelper.GetLinesOfInterest(codeFile.Code, startLine), - Definition = null, - OriginalCode = codeFile.Code, - } - ); - - break; - case { } key when key.StartsWith("reference."): - string referenceChunk = CodeHelper.GetChunkOfLines(codeFile.Code, startLine, endLine); - capturesResult.ReferenceCaptureItems.Add( - new ReferenceCaptureItem - { - CaptureKey = captureKey, - RelativePath = codeFile.RelativePath.NormalizePath(), - CaptureValue = captureValue, - CodeChunk = null, - Definition = captureValue, - OriginalCode = codeFile.Code, - } - ); - break; - case { } key when key.StartsWith("name."): - string codeChunk = CodeHelper.GetLinesOfInterest(codeFile.Code, startLine); - if (language == ProgrammingLanguage.Csharp && key == "name.method") - { - codeChunk = CodeHelper.GetLinesOfInterest( + var startLine = (int)node_start_point(captureNode.Value).row + 1; + var endLine = (int)node_end_point(captureNode.Value).row + 1; + var captureValue = GetValueFromNode(byteArrayCode, captureNode.Value); + + return new DefinitionCaptureItem + { + CaptureKey = captureNode.CaptureKey, + CaptureValue = captureValue, + StartLine = startLine, + EndLine = endLine, + }; + }) + .ToList(); + + var nameCapture = captureItems.FirstOrDefault(x => x.CaptureKey.StartsWith("name.")); + var definitionCapture = captureItems.FirstOrDefault(x => x.CaptureKey.StartsWith("definition.")); + var group = captureList.FirstOrDefault()?.CaptureGroup; + + capturesResult.DefinitionCaptureItems.Add( + new DefinitionCapture + { + CaptureItems = captureItems, + RelativePath = codeFile.RelativePath.NormalizePath(), + CaptureGroup = group ?? string.Empty, + Signiture = + nameCapture != null + ? CodeHelper.GetLinesOfInterest(codeFile.Code, nameCapture.StartLine) + : nameCapture?.CaptureValue, + Definition = + definitionCapture != null + ? CodeHelper.GetChunkOfLines( codeFile.Code, - startLine, - endPatterns: [")"], - stopPatterns: ["{"] - ); - } - - capturesResult.DefinitionCaptureItems.Add( - new DefinitionCaptureItem - { - CaptureKey = captureKey, - RelativePath = codeFile.RelativePath.NormalizePath(), - CaptureValue = captureValue, - CodeChunk = codeChunk, - Definition = null, - OriginalCode = codeFile.Code, - } - ); - - break; - case { } key when key.StartsWith("definition."): - string definitionChunk = CodeHelper.GetChunkOfLines(codeFile.Code, startLine, endLine); - capturesResult.DefinitionCaptureItems.Add( - new DefinitionCaptureItem - { - CaptureKey = captureKey, - RelativePath = codeFile.RelativePath.NormalizePath(), - CaptureValue = captureValue, - CodeChunk = null, - Definition = captureValue, - OriginalCode = codeFile.Code, - } - ); - break; - default: - break; + definitionCapture.StartLine, + definitionCapture.EndLine + ) + : definitionCapture?.CaptureValue, + OriginalCode = codeFile.Code, } - } + ); } - - // cleanup resources - query_cursor_delete(queryCursor); - query_delete(defaultQuery); - tree_delete(tree); - parser_delete(parser); } } - LinkReferencesToDefinitions(capturesResult); - return capturesResult.DefinitionCaptureItems.ToList().AsReadOnly(); } + private static unsafe string GetCaptureKey(TSQuery* defaultQuery, TSQueryCapture capture) + { + uint length; + sbyte* captureNamePtr = query_capture_name_for_id(defaultQuery, capture.index, &length); + string captureKey = new GeneratedCString(captureNamePtr); + + return captureKey; + } + private static string GetValueFromNode(byte[] byteArrayCode, TSNode node) { var startByte = node_start_byte(node); @@ -202,30 +173,9 @@ private static string GetValueFromNode(byte[] byteArrayCode, TSNode node) return matchedCode.Trim(); } - private static void LinkReferencesToDefinitions(CapturesResult capturesResult) + private static string GetCaptureGroupId(string captureKey) { - var definitions = capturesResult.DefinitionCaptureItems; - - foreach (var definition in definitions) - { - var relatedReferences = capturesResult - .ReferenceCaptureItems.Where(reference => reference.CaptureValue == definition.CaptureValue) - .ToList(); - - if (relatedReferences.Count != 0) - { - relatedReferences.ForEach(x => - { - definition.DefinitionCaptureReferences.Add( - new DefinitionCaptureReference - { - RelativePath = x.RelativePath.NormalizePath(), - ReferencedValue = x.CaptureValue, - ReferencedUsage = x.CaptureKey, - } - ); - }); - } - } + var index = captureKey.IndexOf('.', StringComparison.Ordinal); + return index >= 0 ? captureKey.Substring(index + 1) : captureKey; } } diff --git a/src/TreeSitter.Bindings/Services/TreeStructureGeneratorService.cs b/src/TreeSitter.Bindings/Services/TreeStructureGeneratorService.cs index 7ea25a6..f997fda 100644 --- a/src/TreeSitter.Bindings/Services/TreeStructureGeneratorService.cs +++ b/src/TreeSitter.Bindings/Services/TreeStructureGeneratorService.cs @@ -12,78 +12,97 @@ public string GenerateOriginalCodeTree(string originalCode, string relativePath) var sb = new StringBuilder(); sb.AppendLine($"{relativePath.NormalizePath()}:"); - WriteRootCodeLine(sb, "│ ", originalCode); + sb.AppendLine("│"); + + var indent = "│ "; + sb.AppendLine("├── definition: "); + WriteMultiLine(sb, indent, originalCode); return sb.ToString(); } - public string GenerateTreeSitter(IList definitionItems, bool isFull) + public string GenerateTreeSitter(IList definitionCaptures, bool isFull) { var sb = new StringBuilder(); - var relativePath = definitionItems.FirstOrDefault()?.RelativePath.NormalizePath() ?? "Unknown File"; + var normalizedRelativePath = definitionCaptures.First().RelativePath.NormalizePath(); - sb.AppendLine($"{relativePath}:"); + // Start tree with file path + sb.AppendLine($"{normalizedRelativePath}:"); sb.AppendLine("│"); - var groupedItems = definitionItems - .GroupBy(item => item.CaptureKey) - .Select(group => new { CaptureKey = group.Key, Values = group.ToList() }) - .Where(x => - x.Values.All(v => - (!string.IsNullOrWhiteSpace(v.CodeChunk) && !isFull) - || (!string.IsNullOrWhiteSpace(v.Definition) && isFull) - ) - ) - .OrderBy(g => g.CaptureKey) + // Group definition items by their capture group + var groupedItems = definitionCaptures + .GroupBy(item => item.CaptureGroup) + .Select(group => new { CaptureKey = group.Key, Items = group.ToList() }) + .OrderBy(group => group.CaptureKey) .ToList(); - foreach (var groupedItem in groupedItems) + foreach (var group in groupedItems) { - // Add the CaptureKey as a top-level tree node - sb.AppendLine($"├── {GetNormalizedKeyName(groupedItem.CaptureKey)}:"); - // Recursively add children for each CaptureValue under this CaptureKey - AddChildItems(sb, groupedItem.Values, "│ "); + foreach (var definitionCapture in group.Items) + { + var captureGroup = group.CaptureKey; + sb.AppendLine($"├── {captureGroup}:"); + + AddCaptureItems(sb, definitionCapture.CaptureItems, "│ ", isFull); + + if (!isFull) + { + AddSigniture(sb, definitionCapture, "│ "); + } + } } return sb.ToString(); } - private static void AddChildItems(StringBuilder sb, List items, string indent) + private static void AddCaptureItems( + StringBuilder sb, + IList items, + string indent, + bool isFull + ) { foreach (var item in items) { - var code = !string.IsNullOrWhiteSpace(item.CodeChunk) ? item.CodeChunk : item.Definition; - if (string.IsNullOrWhiteSpace(code)) + var normalizedKeyProperty = GetNormalizedKeyProperty(item.CaptureKey); + + if (normalizedKeyProperty.StartsWith("definition") && !isFull) + { continue; + } - // Add first line (signature or declaration) with current indentation - WriteChildrenCodeLine(sb, indent, code); - } + if (normalizedKeyProperty.StartsWith("definition") && isFull) + { + sb.AppendLine($"{indent}├── definition: "); + WriteMultiLine(sb, indent, item.CaptureValue); + continue; + } - // Add omitted code indicator if there are no further child nodes - if (items.Count != 0) - { - sb.AppendLine($"{indent}⋮..."); + WriteSingleLine(sb, normalizedKeyProperty, item.CaptureValue, indent); } } - private static string GetNormalizedKeyName(string input) + private static void AddSigniture(StringBuilder sb, DefinitionCapture item, string indent) { - string[] prefixes = { "name.", "reference.", "definition.", "reference_name" }; + if (string.IsNullOrWhiteSpace(item.Signiture)) + return; - foreach (var prefix in prefixes) - { - if (input.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return input.Substring(prefix.Length); - } - } + WriteSingleLine(sb, "signiture", item.Signiture, indent); + } - return input; + private static void WriteSingleLine( + StringBuilder sb, + string normalizedCaptureKey, + string captureValue, + string indent + ) + { + sb.AppendLine($"{indent}├── {normalizedCaptureKey}: {captureValue}"); } - private static void WriteChildrenCodeLine(StringBuilder sb, string indent, string itemDefinition) + private static void WriteMultiLine(StringBuilder sb, string indent, string itemDefinition) { // Split lines into an array while preserving leading whitespace and handling blank lines var lines = itemDefinition @@ -94,14 +113,11 @@ private static void WriteChildrenCodeLine(StringBuilder sb, string indent, strin if (lines.Length == 0) return; - // Write the first line (e.g., method signature or declaration) with a tree branch - sb.AppendLine($"{indent}├── {lines[0]}"); - // Apply additional indentation to method bodies or any nested blocks string lineIndent = indent + "│ "; - for (int i = 1; i < lines.Length; i++) + foreach (var line in lines) { - if (string.IsNullOrWhiteSpace(lines[i])) + if (string.IsNullOrWhiteSpace(line)) { // Preserve blank lines with appropriate tree indentation sb.AppendLine(lineIndent); @@ -109,14 +125,14 @@ private static void WriteChildrenCodeLine(StringBuilder sb, string indent, strin else { // Indent content lines correctly under the method or block - sb.AppendLine($"{lineIndent}{lines[i]}"); + sb.AppendLine($"{lineIndent}{line}"); } } } - private static void WriteRootCodeLine(StringBuilder sb, string indent, string originalCode) + private static string GetNormalizedKeyProperty(string key) { - sb.AppendLine("├── code:"); - WriteChildrenCodeLine(sb, indent, originalCode); + var index = key.IndexOf('.', StringComparison.Ordinal); + return index >= 0 ? key.Substring(0, index) : key; } } diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs b/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs index 3b2b8ec..2c2aea7 100644 --- a/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs +++ b/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs @@ -31,4 +31,19 @@ public void Test2() ); AnsiConsole.Write(s); } + + [Fact] + public void Test3() + { + var s = new SpectreMarkdown( + @"To add a method overload to the `Add` class, we need to modify the `Add` class in the `Models/Add.cs` file. Since the current context does not provide the full implementation of the `Add` class, I will assume a basic structure and add an overloaded method. + +Here's the updated code: + +Update: Models/Add.cs +```csharp +namespace Calculator;" + ); + AnsiConsole.Write(s); + } }