diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d30576a..2e89884 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -131,8 +131,10 @@ jobs: # https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-pack - name: Pack NuGet Package Version ${{ steps.get_version.outputs.nuget_version }} run: dotnet pack src/AIAssist/AIAssist.csproj -o ${{ env.NuGetDirectory }} -c Release --no-restore --no-build + # Publish the NuGet package as an artifact, so they can be used in the following jobs - - uses: actions/upload-artifact@v4 + - name: Upload Package Version ${{ steps.get_version.outputs.nuget_version }} + uses: actions/upload-artifact@v4 with: name: nuget if-no-files-found: error @@ -149,8 +151,10 @@ jobs: with: # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/cloudbuild.md#github-actions fetch-depth: 0 # doing deep clone and avoid shallow clone so nbgv can do its work. + # Download the NuGet package created in the previous job and copy in the root - - uses: actions/download-artifact@v4 + - name: Download Nuget + uses: actions/download-artifact@v4 with: name: nuget ## Optional. Default is $GITHUB_WORKSPACE diff --git a/AIAssistant.sln b/AIAssistant.sln index 25f73fc..f027357 100644 --- a/AIAssistant.sln +++ b/AIAssistant.sln @@ -82,6 +82,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AIAssist", "AIAssist", "{4F EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIAssist", "src\AIAssist\AIAssist.csproj", "{A4801AE4-5836-47CF-8AA4-DF99918BE2CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildingBlocks.UnitTests", "tests\UnitTests\BuildingBlocks.UnitTests\BuildingBlocks.UnitTests.csproj", "{496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -105,6 +107,7 @@ Global {FA8BD80F-036B-4D0C-BE2D-62EA5FAF9C8C} = {6456834B-EA6C-48EA-9434-A4185D70F65F} {4FAC7598-86FC-495F-B310-C641F424A904} = {97D741A1-DEFC-4241-99BC-7123A2D981E4} {A4801AE4-5836-47CF-8AA4-DF99918BE2CC} = {4FAC7598-86FC-495F-B310-C641F424A904} + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D} = {CF49F264-D5C4-4A9F-BF9C-8706559CFC53} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4973B5D3-67CE-47CC-A0C7-55EC268A2268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -123,5 +126,9 @@ Global {A4801AE4-5836-47CF-8AA4-DF99918BE2CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4801AE4-5836-47CF-8AA4-DF99918BE2CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4801AE4-5836-47CF-8AA4-DF99918BE2CC}.Release|Any CPU.Build.0 = Release|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {496F9F39-89E5-4F9F-9DF4-8D5A86236C1D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 82ed1da..c33c639 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # AI Assist -> `Context Aware` AI assistant for coding, chat, code explanation, review with supporting local and online language models. +> `Context Aware` AI coding assistant inside terminal to help in code development, code explanation, code refactor and review, bug fix and chat with supporting local and online language models -`AIAssist` is compatible with [OpenAI](https://platform.openai.com/docs/api-reference/introduction) and [Azure AI Services](https://azure.microsoft.com/en-us/products/ai-services) through apis and [Ollama models](https://ollama.com/search) through [ollama engine](https://ollama.com/) locally. +`AIAssist` is compatible with bellow AI Services: +- [x] [OpenAI](https://platform.openai.com/docs/api-reference/introduction) through apis +- [x] [Azure AI Services](https://azure.microsoft.com/en-us/products/ai-services) through apis +- [x] [Ollama](https://ollama.com/) with using [ollama models](https://ollama.com/search) locally +- [ ] [Anthropic](https://docs.anthropic.com/en/api/getting-started) through apis +- [ ] [OpenRouter](https://openrouter.ai/docs/quick-start) through apis > [!TIP] > You can use ollama and its models that are more compatible with code like [deepseek-v2.5](https://ollama.com/library/deepseek-v2.5) or [qwen2.5-coder](https://ollama.com/library/qwen2.5-coder) locally. To use local models, you will need to run [Ollama](https://github.com/ollama/ollama) process first. For running ollama you can use [ollama docker](https://ollama.com/blog/ollama-is-now-available-as-an-official-docker-image) container. @@ -30,16 +35,17 @@ AIAssist uses [Azure AI Services](https://azure.microsoft.com/en-us/products/ai-services) or [OpenAI](https://platform.openai.com/docs/api-reference/introduction) apis by default. For using `OpenAI` or `Azure AI` apis we need to have a `ApiKey`. -- Install `aiassist` with `dotnet tool install ` and bellow command: +- To access `dotnet tool`, we need to install [latest .net sdk](https://dotnet.microsoft.com/en-us/download) first. +- Install `aiassist` with `dotnet tool install` and bellow command: ```bash -TODO: Add Nuget Soon +dotnet tool install --global AIAssist ``` -- For OpenAI If you don't have a API key you can [sign up](https://platform.openai.com/signup) in OpenAI and get a ApiKey. -- For Azure AI service you can [signup](https://azure.microsoft.com/en-us/products/ai-services) a azure account and get a AI model API key. -- After getting Api key we should set API key for chat and embedding models through environment variable or command options. -- Now got to `project directory` with `cd` command in terminal, For running `aiassist` and setting api key. +- For OpenAI If you don't have a API key you can [sign up](https://platform.openai.com/signup) in OpenAI and get a ApiKey. +- For Azure AI service you can [signup](https://azure.microsoft.com/en-us/products/ai-services) a azure account and get a AI model API key. +- After getting Api key we should set API key for chat and embedding models through environment variable or command options. +- Now got to `project directory` with `cd` command in terminal, For running `aiassist` and setting api key. ```bash # Go to project directory @@ -49,14 +55,12 @@ cd /to/project/directory - Set `Api Key` through `environment variable`: Linux terminal: - ```bash export CHAT_MODEL_API_KEY=your-chat-api-key-here export EMBEDDINGS_MODEL_API_KEY=your-embedding-api-key-here ``` Windows Powershell Terminal: - ```powershell $env:CHAT_MODEL_API_KEY=your-chat-api-key-here $env:EMBEDDINGS_MODEL_API_KEY=your-embedding-api-key-here @@ -72,7 +76,6 @@ aiassist code --chat-api-key your-chat-api-key-here --embeddings-api-key your-e - Set `ApiVersion`, `DeploymentId` and `BaseAddress` through`environment variable`: Linux terminal: - ```bash export CHAT_BASE_ADDRESS=your-chat-base-address-here export CHAT_API_VERSION=your-chat-api-version-here @@ -83,7 +86,6 @@ export EMBEDDINGS_DEPLOYMENT_ID=your-embedding-deployment-id-here ``` Windows Powershell Terminal: - ```powershell $env:CHAT_BASE_ADDRESS=your-chat-base-address-here $env:CHAT_API_VERSION=your-chat-api-version-here diff --git a/src/AIAssist/AIAssist.nuspec b/src/AIAssist/AIAssist.nuspec index 6089a18..6ce5b96 100644 --- a/src/AIAssist/AIAssist.nuspec +++ b/src/AIAssist/AIAssist.nuspec @@ -7,9 +7,6 @@ - - - AIAssist AIAssist diff --git a/src/AIAssist/Commands/CodeAssistCommand.cs b/src/AIAssist/Commands/CodeAssistCommand.cs index 7d2c683..fe30c6e 100644 --- a/src/AIAssist/Commands/CodeAssistCommand.cs +++ b/src/AIAssist/Commands/CodeAssistCommand.cs @@ -121,17 +121,11 @@ public override async Task ExecuteAsync(CommandContext context, Settings se SetupOptions(settings); - spectreUtilities.InformationText("Code assist mode is activated!"); - spectreUtilities.InformationText($"Chat model: {_chatModel.Name}"); - - if (_embeddingModel is not null) - { - spectreUtilities.InformationText($"Embedding model: {_embeddingModel.Name}"); - } - - spectreUtilities.InformationText($"CodeAssistType: {_chatModel.ModelOption.CodeAssistType}"); - spectreUtilities.InformationText($"CodeDiffType: {_chatModel.ModelOption.CodeDiffType}"); - spectreUtilities.InformationText("Please 'Ctrl+H' to see all available commands in the code assist mode."); + spectreUtilities.SummaryTextLine("Code assist mode is activated!"); + spectreUtilities.SummaryTextLine( + $"Chat model: {_chatModel.Name} | Embedding model: {_embeddingModel?.Name ?? "-"} | CodeAssistType: {_chatModel.ModelOption.CodeAssistType} | CodeDiffType: {_chatModel.ModelOption.CodeDiffType}" + ); + spectreUtilities.SummaryTextLine("Please 'Ctrl+H' to see all available commands in the code assist mode."); spectreUtilities.WriteRule(); await AnsiConsole @@ -166,7 +160,7 @@ await AnsiConsole if (string.IsNullOrEmpty(userInput)) { - spectreUtilities.ErrorText("Input can't be null or empty string."); + spectreUtilities.ErrorTextLine("Input can't be null or empty string."); continue; } @@ -251,25 +245,22 @@ private void SetupOptions(Settings settings) if (settings.CodeDiffType is not null) { - _chatModel.ModelOption.CodeDiffType = settings.CodeDiffType.Value; + _llmOptions.CodeDiffType = settings.CodeDiffType.Value; } if (settings.CodeAssistType is not null) { - _chatModel.ModelOption.CodeAssistType = settings.CodeAssistType.Value; + _llmOptions.CodeAssistType = settings.CodeAssistType.Value; } if (settings.Threshold is not null && _embeddingModel is not null) { - _embeddingModel.ModelOption.Threshold = settings.Threshold.Value; + _llmOptions.Threshold = settings.Threshold.Value; } if (settings.Temperature is not null) { - _chatModel.ModelOption.Temperature = settings.Temperature.Value; - - if (_embeddingModel is not null) - _embeddingModel.ModelOption.Temperature = settings.Temperature.Value; + _llmOptions.Temperature = settings.Temperature.Value; } } } diff --git a/src/AIAssist/Commands/GenerateCommand.cs b/src/AIAssist/Commands/GenerateCommand.cs new file mode 100644 index 0000000..49f4672 --- /dev/null +++ b/src/AIAssist/Commands/GenerateCommand.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace AIAssist.Commands; + +[Description("Generate some settings and configs for the AI Assist.")] +public class GenerateCommand : AsyncCommand +{ + [CommandOption("-c|--config")] + [Description("[grey] generate base config file for the AIAssist.[/].")] + public bool GenerateConfig { get; set; } + + [CommandOption("-i|--ignore")] + [Description("[grey] generate AIAssist ignore file.[/].")] + public bool GenerateIgnore { get; set; } + + public sealed class Settings : CommandSettings { } + + public override Task ExecuteAsync(CommandContext context, Settings settings) + { + return null; + } +} diff --git a/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs b/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs index f8b84ec..cd003d9 100644 --- a/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/AddFileCommand.cs @@ -38,7 +38,7 @@ public Task ExecuteAsync(IServiceScope scope, string? input) } else { - spectreUtilities.ErrorText($"The specified path does not exist: {path}"); + spectreUtilities.ErrorTextLine($"The specified path does not exist: {path}"); } } @@ -56,7 +56,7 @@ public Task ExecuteAsync(IServiceScope scope, string? input) } } - spectreUtilities.InformationText( + spectreUtilities.InformationTextLine( filesToAdd.Count != 0 ? $"Files added: {string.Join(", ", filesToAdd)}" : "No files were added." ); diff --git a/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs b/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs index af8623b..ae75174 100644 --- a/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/ClearHistoryCommand.cs @@ -16,7 +16,7 @@ public class ClearHistoryCommand(ISpectreUtilities spectreUtilities, IOptions ExecuteAsync(IServiceScope scope, string? input) { - spectreUtilities.InformationText("History cleared."); + spectreUtilities.InformationTextLine("History cleared."); return Task.FromResult(true); } diff --git a/src/AIAssist/Commands/InternalCommands/QuitCommand.cs b/src/AIAssist/Commands/InternalCommands/QuitCommand.cs index 0196872..3b19b2c 100644 --- a/src/AIAssist/Commands/InternalCommands/QuitCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/QuitCommand.cs @@ -15,7 +15,7 @@ public class QuitCommand(ISpectreUtilities spectreUtilities, IOptions ExecuteAsync(IServiceScope scope, string? input) { - spectreUtilities.ErrorText("Process interrupted. Exiting..."); + spectreUtilities.ErrorTextLine("Process interrupted. Exiting..."); // stop running commands return Task.FromResult(false); diff --git a/src/AIAssist/Commands/InternalCommands/RunCommand.cs b/src/AIAssist/Commands/InternalCommands/RunCommand.cs index 76c8810..5e729b8 100644 --- a/src/AIAssist/Commands/InternalCommands/RunCommand.cs +++ b/src/AIAssist/Commands/InternalCommands/RunCommand.cs @@ -50,7 +50,7 @@ public async Task ExecuteAsync(IServiceScope scope, string? input) var fullFilesContentForContext = await codeAssistantManager.GetCodeTreeContentsFromCache(requiredFiles); var newQueryWithAddedFiles = promptManager.FilesAddedToChat(fullFilesContentForContext); - spectreUtilities.SuccessText( + spectreUtilities.SuccessTextLine( $"{string.Join(",", requiredFiles.Select(file => $"'{file}'"))} added to the context." ); @@ -84,5 +84,6 @@ private void PrintChatCost(ChatHistoryItem lastChatHistoryItem) return; spectreUtilities.WriteRule(); spectreUtilities.InformationText(message: lastChatHistoryItem.ChatCost.ToString(), justify: Justify.Right); + spectreUtilities.WriteRule(); } } diff --git a/src/AIAssist/Diff/CodeDiffUpdater.cs b/src/AIAssist/Diff/CodeDiffUpdater.cs index 6e1ea3c..d601267 100644 --- a/src/AIAssist/Diff/CodeDiffUpdater.cs +++ b/src/AIAssist/Diff/CodeDiffUpdater.cs @@ -12,7 +12,7 @@ public void ApplyChanges(IList diffResults, string contextWorkingDir if (string.IsNullOrWhiteSpace(contextWorkingDirectory)) { - spectreUtilities.ErrorText("Working directory cannot be null or whitespace."); + spectreUtilities.ErrorTextLine("Working directory cannot be null or whitespace."); } foreach (var diffResult in diffResults) @@ -53,11 +53,11 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD { var updatedLines = ApplyReplacements(new List(), diffResult.Replacements); File.WriteAllText(modifiedFilePath, string.Join("\n", updatedLines)); - spectreUtilities.SuccessText($"File created: {modifiedFilePath}"); + spectreUtilities.SuccessTextLine($"File created: {modifiedFilePath}"); } else { - spectreUtilities.ErrorText("No modified lines provided for new file creation."); + spectreUtilities.ErrorTextLine("No modified lines provided for new file creation."); } } else if (diffResult.ModifiedPath == noneExistPath && diffResult.OriginalPath != noneExistPath) @@ -68,11 +68,11 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD if (File.Exists(originalFilePath) && diffResult.Replacements is not null && diffResult.Replacements.Any()) { File.Delete(originalFilePath); - spectreUtilities.SuccessText($"File deleted: {originalFilePath}"); + spectreUtilities.SuccessTextLine($"File deleted: {originalFilePath}"); } else { - spectreUtilities.ErrorText($"File not found for deletion: {originalFilePath}"); + spectreUtilities.ErrorTextLine($"File not found for deletion: {originalFilePath}"); } } else if (diffResult.OriginalPath != diffResult.ModifiedPath) @@ -94,7 +94,7 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD } else { - spectreUtilities.ErrorText($"Original file not found for rename/move: {originalFilePath}"); + spectreUtilities.ErrorTextLine($"Original file not found for rename/move: {originalFilePath}"); } } else @@ -108,7 +108,7 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD if (!File.Exists(originalFilePath)) { - spectreUtilities.ErrorText($"Original file not found: {originalFilePath}"); + spectreUtilities.ErrorTextLine($"Original file not found: {originalFilePath}"); } var originalLines = File.ReadAllLines(originalFilePath).ToList(); @@ -117,7 +117,7 @@ private void HandleReplacementFile(DiffResult diffResult, string contextWorkingD Directory.CreateDirectory(Path.GetDirectoryName(modifiedFilePath)!); File.WriteAllText(modifiedFilePath, string.Join("\n", updatedLines)); - spectreUtilities.SuccessText($"File updated: {modifiedFilePath}"); + spectreUtilities.SuccessTextLine($"File updated: {modifiedFilePath}"); } } @@ -167,7 +167,7 @@ string contextWorkingDirectory Directory.CreateDirectory(Path.GetDirectoryName(modifiedFullPath)!); // Normalize and write lines to prevent extra blank lines because WriteAllLines File.WriteAllText(modifiedFullPath, string.Join("\n", modifiedLines)); - spectreUtilities.SuccessText($"File created: {modifiedPath}"); + spectreUtilities.SuccessTextLine($"File created: {modifiedPath}"); break; } @@ -180,11 +180,11 @@ string contextWorkingDirectory { // Normalize and write lines to prevent blank lines File.WriteAllText(modifiedFullPath, string.Join("\n", modifiedLines)); - spectreUtilities.SuccessText($"File updated: {modifiedPath}"); + spectreUtilities.SuccessTextLine($"File updated: {modifiedPath}"); } else { - spectreUtilities.ErrorText($"File {modifiedPath} does not exist to modify."); + spectreUtilities.ErrorTextLine($"File {modifiedPath} does not exist to modify."); } break; } @@ -195,23 +195,23 @@ string contextWorkingDirectory if (File.Exists(originalFullPath)) { File.Delete(originalFullPath); - spectreUtilities.SuccessText($"File deleted: {originalPath}"); + spectreUtilities.SuccessTextLine($"File deleted: {originalPath}"); } else { - spectreUtilities.ErrorText($"File {originalPath} not found for deletion."); + spectreUtilities.ErrorTextLine($"File {originalPath} not found for deletion."); } break; } default: - spectreUtilities.ErrorText($"Unsupported action type: {actionType}"); + spectreUtilities.ErrorTextLine($"Unsupported action type: {actionType}"); break; } } catch (Exception ex) { - spectreUtilities.ErrorText($"Failed to update file {modifiedPath} \n {ex.Message}"); + spectreUtilities.ErrorTextLine($"Failed to update file {modifiedPath} \n {ex.Message}"); } } } diff --git a/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs b/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs index 7deb13c..469f063 100644 --- a/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs +++ b/src/AIAssist/Services/CodeAssistStrategies/EmbeddingCodeAssist.cs @@ -121,5 +121,6 @@ private void PrintEmbeddingCost(int totalCount, decimal totalCost) message: $"Total Embedding Tokens: {totalCount.FormatCommas()} | Total Embedding Cost: ${totalCost.FormatCommas()}", justify: Justify.Right ); + spectreUtilities.WriteRule(); } } diff --git a/src/AIAssist/aiassist-config.json b/src/AIAssist/aiassist-config.json index 77b9baf..cf2dda3 100644 --- a/src/AIAssist/aiassist-config.json +++ b/src/AIAssist/aiassist-config.json @@ -1,12 +1,14 @@ { - "Test":"Test1", "AppOptions": { "ThemeName": "dracula", "PrintCostEnabled": true }, "LLMOptions": { - "ChatModel": "ollama/llama3", - "EmbeddingsModel": "ollama/nomic-embed-text" + "ChatModel": "azure/gpt-4o", + "EmbeddingsModel": "azure/text-embedding-3-large", + "CodeDiffType": "CodeBlockDiff", + "CodeAssistType": "Embedding", + "Temperature": 0.2 }, "Serilog": { "MinimumLevel": { diff --git a/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs b/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs index 9873dd8..17a770d 100644 --- a/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs +++ b/src/BuildingBlocks/MarkdigMarkdown/MarkdownParser.cs @@ -7,10 +7,14 @@ public class MarkdownParser { public MarkdownDocument ToMarkdownDocument(string markdown, string theme = "dracula") { - var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().UseColorCodeBlock(theme).Build(); + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseSoftlineBreakAsHardlineBreak() + .UseColorCodeBlock(theme) + .Build(); // parser Markdown text to markdig documents object - var markdownDocument = global::Markdig.Markdown.Parse(markdown, pipeline); + var markdownDocument = Markdown.Parse(markdown, pipeline); return markdownDocument; } diff --git a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs index 5893977..a4b33a4 100644 --- a/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/Contracts/ISpectreUtilities.cs @@ -6,11 +6,18 @@ public interface ISpectreUtilities { bool ConfirmationPrompt(string message); string? UserPrompt(string? promptMessage = null); + void InformationTextLine(string message, Justify? justify = null, Overflow? overflow = null); void InformationText(string message, Justify? justify = null, Overflow? overflow = null); + public void SummaryTextLine(string message, Justify? justify = null, Overflow? overflow = null); + public void SummaryText(string message, Justify? justify = null, Overflow? overflow = null); + public void HighlightTextLine(string message, Justify? justify = null, Overflow? overflow = null); + public void HighlightText(string message, Justify? justify = null, Overflow? overflow = null); + void NormalTextLine(string message, Justify? justify = null, Overflow? overflow = null); void NormalText(string message, Justify? justify = null, Overflow? overflow = null); + void WarningTextLine(string message, Justify? justify = null, Overflow? overflow = null); void WarningText(string message, Justify? justify = null, Overflow? overflow = null); - void ErrorText(string message, Justify? justify = null, Overflow? overflow = null); - void SuccessText(string message, Justify? justify = null, Overflow? overflow = null); + void ErrorTextLine(string message, Justify? justify = null, Overflow? overflow = null); + void SuccessTextLine(string message, Justify? justify = null, Overflow? overflow = null); void WriteCursor(); void WriteRule(); void Exception(string errorMessage, Exception ex); diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreCompositeRenderable.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreCompositeRenderable.cs deleted file mode 100644 index 4cdb6e8..0000000 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreCompositeRenderable.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Spectre.Console.Rendering; - -namespace BuildingBlocks.SpectreConsole.Markdown; - -internal class SpectreCompositeRenderable(IEnumerable renderables) : Renderable -{ - protected override IEnumerable Render(RenderOptions options, int maxWidth) - { - return renderables.SelectMany(x => x.Render(options, maxWidth)); - } -} diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs index 4b36dc9..6cdce6c 100644 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownBlockRendering.cs @@ -36,84 +36,53 @@ public IRenderable RenderBlock( bool suppressNewLine = false ) { - IRenderable? result = null; - switch (markdigBlock) + return markdigBlock switch { - case CustomContainer: - case MathBlock: - case Footnote: - case FootnoteGroup: - case FootnoteLinkReferenceDefinition: - case Figure: - case FigureCaption: - case YamlFrontMatterBlock: - case Abbreviation: - case FooterBlock: - case HtmlBlock: - case DefinitionItem: - case DefinitionList: - case DefinitionTerm: - break; - - // No point rendering these as the definitions are already reconciled by the parser. - case HeadingLinkReferenceDefinition: - case LinkReferenceDefinitionGroup: - case LinkReferenceDefinition: - break; - - case BlankLineBlock: - return new Text(Environment.NewLine); - case EmptyBlock: - return new Text(Environment.NewLine); - case Table table: - result = RenderTableBlock(table); - break; - case FencedCodeBlock fencedCodeBlock: - return RenderFenceBlock(fencedCodeBlock); - case CodeBlock codeBlock: - var blockContents = codeBlock.Lines.ToString(); - result = new Panel(blockContents) - { - Header = new PanelHeader("code"), - Expand = true, - Border = BoxBorder.Rounded, - }; - break; - case ListBlock listBlock: - result = RenderListBlock(listBlock, CreateStyle(_colorTheme.ListStyle, style)); - break; - case ListItemBlock listItemBlock: - result = RenderListItemBlock(listItemBlock, style); - break; - case QuoteBlock quoteBlock: - result = RenderQuoteBlock(quoteBlock, CreateStyle(_colorTheme.BlockQuoteStyle, style)); - break; - case HeadingBlock headingBlock: - result = RenderHeadingBlock(headingBlock, CreateStyle(_colorTheme.HeadStyle, style)); - break; - case ParagraphBlock paragraphBlock: - if (suppressNewLine) - return RenderParagraphBlock( - paragraphBlock, - alignment, - CreateStyle(_colorTheme.ParagraphStyle, style), - suppressNewLine - ); - result = RenderParagraphBlock( - paragraphBlock, - alignment, - CreateStyle(_colorTheme.ParagraphStyle, style), - suppressNewLine - ); - break; - case ThematicBreakBlock thematicBreakBlock: - return new Text(thematicBreakBlock.Content.Text); - } + BlankLineBlock => new Text(Environment.NewLine), + EmptyBlock => new Text(string.Empty), + Table table => AppendBreakAfter(RenderTableBlock(table, style)), + FencedCodeBlock fencedCodeBlock => AppendBreakAfter(RenderFenceBlock(fencedCodeBlock)), + CodeBlock codeBlock => AppendBreakAfter(RenderCodeBlock(codeBlock)), + ListBlock listBlock => AppendBreakBeforeAfter( + RenderListBlock(listBlock, CreateStyle(_colorTheme.ListStyle, style)) + ), + ListItemBlock listItemBlock => RenderListItemBlock(listItemBlock, style), + QuoteBlock quoteBlock => AppendBreakAfter( + RenderQuoteBlock(quoteBlock, CreateStyle(_colorTheme.BlockQuoteStyle, style)) + ), + HeadingBlock headingBlock => AppendBreakAfter( + RenderHeadingBlock(headingBlock, CreateStyle(_colorTheme.HeadStyle, style)) + ), + ParagraphBlock paragraphBlock => RenderParagraphBlock( + paragraphBlock, + alignment, + CreateStyle(_colorTheme.ParagraphStyle, style), + suppressNewLine + ), + ThematicBreakBlock thematicBreakBlock => new Text(thematicBreakBlock.Content.Text), + _ => Text.Empty, + }; + } - if (result is not null) - return new SpectreCompositeRenderable(new List { result, new Text(Environment.NewLine) }); + private IRenderable AppendBreakAfter(IRenderable renderable) + { + return new SpectreVerticalCompositeRenderable( + new List { renderable, new Text(Environment.NewLine) } + ); + } - return Text.Empty; + private IRenderable AppendBreakBefore(IRenderable renderable) + { + return new SpectreVerticalCompositeRenderable( + new List { new Text(Environment.NewLine), renderable } + ); + } + + private IRenderable AppendBreakBeforeAfter(IRenderable renderable) + { + return new SpectreVerticalCompositeRenderable( + new List { new Text(Environment.NewLine), renderable, new Text(Environment.NewLine) } + ); } private IRenderable RenderFenceBlock(FencedCodeBlock fencedCodeBlock) @@ -131,11 +100,22 @@ private IRenderable RenderFenceBlock(FencedCodeBlock fencedCodeBlock) ).PadLeft(_colorTheme.CodeBlockStyle.Margin); } + private IRenderable RenderCodeBlock(CodeBlock codeBlock) + { + var blockContents = codeBlock.Lines.ToString(); + return new Panel(blockContents) + { + Header = new PanelHeader("code"), + Expand = true, + Border = BoxBorder.Rounded, + }; + } + private IRenderable RenderQuoteBlock(QuoteBlock quoteBlock, Style style) { foreach (var subBlock in quoteBlock) if (subBlock is ParagraphBlock paragraph) - return new SpectreCompositeRenderable( + return new SpectreVerticalCompositeRenderable( new List { new Markup( @@ -146,41 +126,57 @@ private IRenderable RenderQuoteBlock(QuoteBlock quoteBlock, Style style) } ); - return new Text(""); + return Text.Empty; } - private IRenderable RenderListBlock(ListBlock listBlock, Style style) + private IRenderable RenderListBlock(ListBlock listBlock, Style style, int indentLevel = 0) { - IEnumerable? itemPrefixes; - if (listBlock.IsOrdered) - { - var startNum = int.Parse(listBlock.OrderedStart); - var orderedDelimiter = listBlock.OrderedDelimiter; - itemPrefixes = Enumerable.Range(startNum, listBlock.Count).Select(num => $"{num}{orderedDelimiter}"); - } - else - { - itemPrefixes = Enumerable.Repeat( - _colorTheme.ListStyle.BlockPrefix ?? CharacterSet.ListBullet, - listBlock.Count - ); - } + int startNumber = int.TryParse(listBlock.OrderedStart, out var parsedNumber) ? parsedNumber : 1; - var paddedItemPrefixes = itemPrefixes.Select(x => new Text( - $" {x} ", - style: Style.Parse(_colorTheme.ListStyle.PrefixForeground ?? "default") - )); + // Generate item prefixes + var itemPrefixes = listBlock.IsOrdered + ? Enumerable.Range(startNumber, listBlock.Count).Select(num => $"{num}{listBlock.OrderedDelimiter}") + : Enumerable.Repeat(_colorTheme.ListStyle.BlockPrefix ?? CharacterSet.ListBullet, listBlock.Count); - return new SpectreCompositeRenderable( - [.. Interleave(paddedItemPrefixes, listBlock.Select(x => RenderBlock(x, style: style)))] - ); + var renderedItems = listBlock + .OfType() + .Select( + (itemBlock, index) => + { + // Generate the prefix for the current list item + var prefix = new Text( + $"{new string(' ', indentLevel * 4)}{itemPrefixes.ElementAt(index)} ", + style: Style.Parse(_colorTheme.ListStyle.PrefixForeground ?? "default") + ); + + var itemContent = RenderListItemBlock(itemBlock, style, indentLevel); + + // Combine the prefix and content + return new SpectreVerticalCompositeRenderable([prefix, itemContent]); + } + ); + + // Combine all rendered items without introducing extra line break + return new SpectreHorizontalCompositeRenderable(renderedItems); } - private IRenderable RenderListItemBlock(ListItemBlock listItemBlock, Style? style = null) + private IRenderable RenderListItemBlock(ListItemBlock listItemBlock, Style? style = null, int indentLevel = 0) { - return new SpectreCompositeRenderable( - listItemBlock.Select(x => RenderBlock(x, suppressNewLine: true, style: style)) - ); + // Render children blocks for the list item + var renderedChildren = listItemBlock.Select(child => + { + if (child is ListBlock nestedList) + { + // Indent nested lists + return RenderListBlock(nestedList, style ?? Style.Plain, indentLevel + 1); + } + + // Maintain indentation for other blocks + return RenderBlock(child, suppressNewLine: true, style: style); + }); + + // Combine all child blocks without unnecessary line break + return new SpectreHorizontalCompositeRenderable(renderedChildren); } private IRenderable RenderTableBlock(Table table, Style? style = null) @@ -189,7 +185,6 @@ private IRenderable RenderTableBlock(Table table, Style? style = null) { var renderedTable = new Spectre.Console.Table(); - // Safe to unconditionally cast to TableRow as IsValid() ensures this is the case under the hood foreach (var tableRow in table.Cast()) if (tableRow.IsHeader) AddColumnsToTable(tableRow, table.ColumnDefinitions, renderedTable, style); @@ -209,7 +204,6 @@ private void AddColumnsToTable( Style? style = null ) { - // Safe to unconditionally cast to TableCell as IsValid() ensures this is the case under the hood foreach (var (cell, def) in tableRow.Cast().Zip(columnDefinitions)) renderedTable.AddColumn(new TableColumn(RenderTableCell(cell, def.Alignment, style))); } @@ -221,11 +215,11 @@ private void AddRowToTable( Style? style = null ) { - var renderedRow = new List(); - - // Safe to unconditionally cast to TableCell as IsValid() ensures this is the case under the hood - foreach (var (cell, def) in tableRow.Cast().Zip(columnDefinitions)) - renderedRow.Add(RenderTableCell(cell, def.Alignment, style)); + var renderedRow = tableRow + .Cast() + .Zip(columnDefinitions) + .Select(cellDef => RenderTableCell(cellDef.First, cellDef.Second.Alignment, style)) + .ToList(); renderedTable.AddRow(renderedRow); } @@ -238,68 +232,37 @@ private IRenderable RenderTableCell(TableCell tableCell, TableColumnAlign? markd TableColumnAlign.Center => Justify.Center, TableColumnAlign.Right => Justify.Right, null => Justify.Left, - _ => throw new ArgumentOutOfRangeException( - nameof(markdownAlignment), - markdownAlignment, - "Unable to convert between Markdig alignment and Spectre.Console alignment" - ), + _ => throw new ArgumentOutOfRangeException(), }; - return new SpectreCompositeRenderable( - tableCell.Select(x => RenderBlock(x, consoleAlignment, style: style, true)) + return new SpectreVerticalCompositeRenderable( + tableCell.Select(x => RenderBlock(x, consoleAlignment, style, true)) ); } - private IRenderable RenderParagraphBlock( - ParagraphBlock paragraphBlock, - Justify alignment, - Style style, - bool suppressNewLine = false - ) - { - var text = _inlineRendering.RenderContainerInline(paragraphBlock.Inline, style, alignment: alignment); - - if (!suppressNewLine) - { - return new SpectreCompositeRenderable(new List { text, new Text(Environment.NewLine) }); - } - - return new SpectreCompositeRenderable(new List { text }); - } - private IRenderable RenderHeadingBlock(HeadingBlock headingBlock, Style style) { var headingText = headingBlock.Inline?.GetInlineContent() ?? string.Empty; - var prefix = new string('#', headingBlock.Level); - return new SpectreCompositeRenderable( - new List { new Markup($" {prefix} {headingText} ", style), new Text(Environment.NewLine) } - ); + return new Markup($" {prefix} {headingText} ", style); } - // write items for each bullet - private static IEnumerable Interleave(IEnumerable seqA, IEnumerable seqB) + private IRenderable RenderParagraphBlock( + ParagraphBlock paragraphBlock, + Justify alignment, + Style style, + bool suppressNewLine = false + ) { - using var enumeratorA = seqA.GetEnumerator(); - using var enumeratorB = seqB.GetEnumerator(); - - while (enumeratorA.MoveNext()) - { - yield return enumeratorA.Current; - - if (enumeratorB.MoveNext()) - yield return enumeratorB.Current; - } + var text = _inlineRendering.RenderContainerInline(paragraphBlock.Inline, style, alignment: alignment); - while (enumeratorB.MoveNext()) - yield return enumeratorB.Current; + return suppressNewLine ? text : AppendBreakAfter(text); } private Style CreateStyle(StyleBase styleBase, Style? style = null) { style ??= Style.Parse(CreateStringStyle(styleBase)); - return style; } @@ -309,20 +272,7 @@ private string CreateStringStyle(StyleBase styleBase) var bold = styleBase.Bold ? "bold" : "default"; var underline = styleBase.Underline ? "underline" : "default"; - var style = - $"{ - styleBase.Foreground ?? "default" - } on { - styleBase.Background ?? "default" - } { - italic - } { - bold - } { - underline - }"; - - return style; + return $"{styleBase.Foreground ?? "default"} on {styleBase.Background ?? "default"} {italic} {bold} {underline}"; } public void Dispose() diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs index de8c92d..5e3eee1 100644 --- a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreMarkdownInlineRendering.cs @@ -21,7 +21,6 @@ private IRenderable RenderInline(Inline inline, Style style, Justify alignment) { switch (inline) { - // TODO: These features are less adopted in practice and the MarkdownPipeline isn't configured to generate them - feel free to add support! case JiraLink jiraLink: case SmartyPant smartyPant: case MathInline mathInline: @@ -90,7 +89,9 @@ public IRenderable RenderContainerInline( Justify alignment = Justify.Left ) { - return new SpectreCompositeRenderable(inline.Select(x => RenderInline(x, style ?? Style.Plain, alignment))); + return new SpectreVerticalCompositeRenderable( + inline.Select(x => RenderInline(x, style ?? Style.Plain, alignment)) + ); } private Markup WriteCodeInline(CodeInline code, Style style) diff --git a/src/BuildingBlocks/SpectreConsole/Markdown/SpectreVerticalCompositeRenderable.cs b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreVerticalCompositeRenderable.cs new file mode 100644 index 0000000..812229f --- /dev/null +++ b/src/BuildingBlocks/SpectreConsole/Markdown/SpectreVerticalCompositeRenderable.cs @@ -0,0 +1,23 @@ +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace BuildingBlocks.SpectreConsole.Markdown; + +internal class SpectreVerticalCompositeRenderable(IEnumerable renderables) : Renderable +{ + protected override IEnumerable Render(RenderOptions options, int maxWidth) + { + return renderables.SelectMany(x => x.Render(options, maxWidth)); + } +} + +// Stacking elements vertically or adding multiple horizontal rows without extra space +internal class SpectreHorizontalCompositeRenderable(IEnumerable renderablesElements) : Renderable +{ + protected override IEnumerable Render(RenderOptions options, int maxWidth) + { + // Use Rows to combine renderables items in multiple horizontal rows without extra space + IRenderable rows = new Rows(renderablesElements); + return rows.Render(options, maxWidth); + } +} diff --git a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs index 27607e5..ade21e9 100644 --- a/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs +++ b/src/BuildingBlocks/SpectreConsole/SpectreUtilities.cs @@ -29,10 +29,16 @@ public bool ConfirmationPrompt(string message) return input; } + public void InformationTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + InformationText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + public void InformationText(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Information)}]{message}[/]" + Environment.NewLine) + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Information)}]{message}[/]") { Overflow = overflow, Justification = justify, @@ -40,10 +46,16 @@ public void InformationText(string message, Justify? justify = null, Overflow? o ); } - public void WarningText(string message, Justify? justify = null, Overflow? overflow = null) + public void SummaryTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + SummaryText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + + public void SummaryText(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Warning)}]{message}[/]" + Environment.NewLine) + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Summary)}]{message}[/]") { Overflow = overflow, Justification = justify, @@ -51,10 +63,50 @@ public void WarningText(string message, Justify? justify = null, Overflow? overf ); } + public void HighlightTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + HighlightText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + + public void HighlightText(string message, Justify? justify = null, Overflow? overflow = null) + { + console.Write( + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Highlight)}]{message}[/]") + { + Overflow = overflow, + Justification = justify, + } + ); + } + + public void NormalTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + NormalText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + public void NormalText(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( - new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Text)}]{message}[/]" + Environment.NewLine) + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Text)}]{message}[/]") + { + Overflow = overflow, + Justification = justify, + } + ); + } + + public void WarningTextLine(string message, Justify? justify = null, Overflow? overflow = null) + { + WarningText(message, justify: justify, overflow: overflow); + console.WriteLine(); + } + + public void WarningText(string message, Justify? justify = null, Overflow? overflow = null) + { + console.Write( + new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Warning)}]{message}[/]") { Overflow = overflow, Justification = justify, @@ -62,7 +114,7 @@ public void NormalText(string message, Justify? justify = null, Overflow? overfl ); } - public void ErrorText(string message, Justify? justify = null, Overflow? overflow = null) + public void ErrorTextLine(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Error)}]{message}[/]" + Environment.NewLine) @@ -73,7 +125,7 @@ public void ErrorText(string message, Justify? justify = null, Overflow? overflo ); } - public void SuccessText(string message, Justify? justify = null, Overflow? overflow = null) + public void SuccessTextLine(string message, Justify? justify = null, Overflow? overflow = null) { console.Write( new Markup($"[{CreateStringStyle(theme.ConsoleStyle.Success)}]{message}[/]" + Environment.NewLine) @@ -96,7 +148,7 @@ public void WriteRule() public void Exception(string errorMessage, Exception ex) { - ErrorText(errorMessage); + ErrorTextLine(errorMessage); console.WriteException(ex, ExceptionFormats.ShortenEverything); } diff --git a/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs b/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs index 4d10e57..1f0eef2 100644 --- a/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs +++ b/src/BuildingBlocks/SpectreConsole/StyleElements/ConsoleStyle.cs @@ -9,6 +9,8 @@ public class ConsoleStyle public StyleBase Success { get; set; } = default!; public StyleBase Tree { get; set; } = default!; public StyleBase Information { get; set; } = default!; + public StyleBase Summary { get; set; } = default!; + public StyleBase Highlight { get; set; } = default!; public StyleBase Warning { get; set; } = default!; public StyleBase Confirmation { get; set; } = default!; } diff --git a/src/BuildingBlocks/SpectreConsole/Themes/dracula.json b/src/BuildingBlocks/SpectreConsole/Themes/dracula.json index 14fccde..c891b13 100644 --- a/src/BuildingBlocks/SpectreConsole/Themes/dracula.json +++ b/src/BuildingBlocks/SpectreConsole/Themes/dracula.json @@ -17,6 +17,12 @@ "confirmation": { "foreground": "#8be9fd" }, + "summary": { + "foreground": "#8be9fd" + }, + "highlight": { + "foreground": "#ff79c6" + }, "error": { "foreground": "#ff5555" }, diff --git a/src/Clients/AnthropicClient.cs b/src/Clients/AnthropicClient.cs index c2ea97d..a6c6581 100644 --- a/src/Clients/AnthropicClient.cs +++ b/src/Clients/AnthropicClient.cs @@ -23,13 +23,16 @@ namespace Clients; public class AnthropicClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( diff --git a/src/Clients/AzureClient.cs b/src/Clients/AzureClient.cs index f333796..313cf6d 100644 --- a/src/Clients/AzureClient.cs +++ b/src/Clients/AzureClient.cs @@ -24,14 +24,16 @@ namespace Clients; public class AzureClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); - private readonly Model _embeddingModel = cacheModels.GetModel(options.Value.EmbeddingsModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( @@ -247,6 +249,7 @@ AsyncPolicyWrap combinedPolicy await ValidateEmbeddingMaxInputToken(string.Concat(inputs)); ValidateRequestSizeAndContent(string.Concat(inputs)); + ArgumentNullException.ThrowIfNull(_embeddingModel); var requestBody = new { input = inputs, @@ -258,11 +261,11 @@ AsyncPolicyWrap combinedPolicy var apiVersion = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsApiVersion) - ?? _chatModel.ModelOption.ApiVersion; + ?? _embeddingModel.ModelOption.ApiVersion; var deploymentId = Environment.GetEnvironmentVariable(ClientsConstants.Environments.EmbeddingsDeploymentId) - ?? _chatModel.ModelOption.DeploymentId; + ?? _embeddingModel.ModelOption.DeploymentId; ArgumentException.ThrowIfNullOrEmpty(apiVersion); ArgumentException.ThrowIfNullOrEmpty(deploymentId); diff --git a/src/Clients/CacheModels.cs b/src/Clients/CacheModels.cs index c188f34..3540703 100644 --- a/src/Clients/CacheModels.cs +++ b/src/Clients/CacheModels.cs @@ -14,12 +14,18 @@ public class CacheModels : ICacheModels { private readonly ModelsInformationOptions _modelsInformation; private readonly ModelsOptions _modelOptions; + private readonly LLMOptions _llmOptions; private readonly Dictionary _models = new(); - public CacheModels(IOptions modelOptions, IOptions modelsInformation) + public CacheModels( + IOptions modelOptions, + IOptions modelsInformation, + IOptions llmOptions + ) { _modelsInformation = modelsInformation.Value; _modelOptions = modelOptions.Value; + _llmOptions = llmOptions.Value; InitCache(); } @@ -85,9 +91,12 @@ private void InitCache() options )!; - foreach (var (originalName, information) in predefinedModelsInformation.Where(x => x.Value.Enabled)) + foreach ( + var (originalName, predefinedModelInformation) in predefinedModelsInformation.Where(x => x.Value.Enabled) + ) { - var modelOption = predefinedModelOptions.GetValueOrDefault(originalName); + var predefinedModelOption = predefinedModelOptions.GetValueOrDefault(originalName); + var overrideModelOption = _modelOptions.GetValueOrDefault(originalName); var overrideModelInformation = _modelsInformation.GetValueOrDefault(originalName); @@ -98,39 +107,62 @@ private void InitCache() ModelOption = new ModelOption { CodeAssistType = - overrideModelOption?.CodeAssistType ?? modelOption?.CodeAssistType ?? CodeAssistType.Embedding, + overrideModelOption?.CodeAssistType + ?? _llmOptions.CodeAssistType + ?? predefinedModelOption?.CodeAssistType + ?? CodeAssistType.Embedding, CodeDiffType = - overrideModelOption?.CodeDiffType ?? modelOption?.CodeDiffType ?? CodeDiffType.CodeBlockDiff, - Threshold = overrideModelOption?.Threshold ?? modelOption?.Threshold ?? 0.4m, - Temperature = overrideModelOption?.Temperature ?? modelOption?.Temperature ?? 0.2m, - ApiVersion = overrideModelOption?.ApiVersion ?? modelOption?.ApiVersion, - BaseAddress = overrideModelOption?.BaseAddress ?? modelOption?.BaseAddress, - DeploymentId = overrideModelOption?.DeploymentId ?? modelOption?.DeploymentId, + overrideModelOption?.CodeDiffType + ?? _llmOptions.CodeDiffType + ?? predefinedModelOption?.CodeDiffType + ?? CodeDiffType.CodeBlockDiff, + Threshold = + overrideModelOption?.Threshold + ?? _llmOptions.Threshold + ?? predefinedModelOption?.Threshold + ?? 0.4m, + Temperature = + overrideModelOption?.Temperature + ?? _llmOptions.Temperature + ?? predefinedModelOption?.Temperature + ?? 0.2m, + ApiVersion = overrideModelOption?.ApiVersion ?? predefinedModelOption?.ApiVersion, + BaseAddress = overrideModelOption?.BaseAddress ?? predefinedModelOption?.BaseAddress, + DeploymentId = overrideModelOption?.DeploymentId ?? predefinedModelOption?.DeploymentId, }, ModelInformation = new ModelInformation { - AIProvider = overrideModelInformation?.AIProvider ?? information.AIProvider, - ModelType = overrideModelInformation?.ModelType ?? information.ModelType, - MaxTokens = overrideModelInformation?.MaxTokens ?? information.MaxTokens, - MaxInputTokens = overrideModelInformation?.MaxInputTokens ?? information.MaxInputTokens, - MaxOutputTokens = overrideModelInformation?.MaxOutputTokens ?? information.MaxOutputTokens, - InputCostPerToken = overrideModelInformation?.InputCostPerToken ?? information.InputCostPerToken, - OutputCostPerToken = overrideModelInformation?.OutputCostPerToken ?? information.OutputCostPerToken, - OutputVectorSize = overrideModelInformation?.OutputVectorSize ?? information.OutputVectorSize, - Enabled = overrideModelInformation?.Enabled ?? information.Enabled, + AIProvider = overrideModelInformation?.AIProvider ?? predefinedModelInformation.AIProvider, + ModelType = overrideModelInformation?.ModelType ?? predefinedModelInformation.ModelType, + MaxTokens = overrideModelInformation?.MaxTokens ?? predefinedModelInformation.MaxTokens, + MaxInputTokens = + overrideModelInformation?.MaxInputTokens ?? predefinedModelInformation.MaxInputTokens, + MaxOutputTokens = + overrideModelInformation?.MaxOutputTokens ?? predefinedModelInformation.MaxOutputTokens, + InputCostPerToken = + overrideModelInformation?.InputCostPerToken ?? predefinedModelInformation.InputCostPerToken, + OutputCostPerToken = + overrideModelInformation?.OutputCostPerToken ?? predefinedModelInformation.OutputCostPerToken, + OutputVectorSize = + overrideModelInformation?.OutputVectorSize ?? predefinedModelInformation.OutputVectorSize, + Enabled = overrideModelInformation?.Enabled ?? predefinedModelInformation.Enabled, SupportsFunctionCalling = - overrideModelInformation?.SupportsFunctionCalling ?? information.SupportsFunctionCalling, + overrideModelInformation?.SupportsFunctionCalling + ?? predefinedModelInformation.SupportsFunctionCalling, SupportsParallelFunctionCalling = overrideModelInformation?.SupportsParallelFunctionCalling - ?? information.SupportsParallelFunctionCalling, - SupportsVision = overrideModelInformation?.SupportsVision ?? information.SupportsVision, + ?? predefinedModelInformation.SupportsParallelFunctionCalling, + SupportsVision = + overrideModelInformation?.SupportsVision ?? predefinedModelInformation.SupportsVision, EmbeddingDimensions = - overrideModelInformation?.EmbeddingDimensions ?? information.EmbeddingDimensions, - SupportsAudioInput = overrideModelInformation?.SupportsAudioInput ?? information.SupportsAudioInput, + overrideModelInformation?.EmbeddingDimensions ?? predefinedModelInformation.EmbeddingDimensions, + SupportsAudioInput = + overrideModelInformation?.SupportsAudioInput ?? predefinedModelInformation.SupportsAudioInput, SupportsAudioOutput = - overrideModelInformation?.SupportsAudioOutput ?? information.SupportsAudioOutput, + overrideModelInformation?.SupportsAudioOutput ?? predefinedModelInformation.SupportsAudioOutput, SupportsPromptCaching = - overrideModelInformation?.SupportsPromptCaching ?? information.SupportsPromptCaching, + overrideModelInformation?.SupportsPromptCaching + ?? predefinedModelInformation.SupportsPromptCaching, }, }; diff --git a/src/Clients/OllamaClient.cs b/src/Clients/OllamaClient.cs index 0796659..ae5c354 100644 --- a/src/Clients/OllamaClient.cs +++ b/src/Clients/OllamaClient.cs @@ -24,14 +24,16 @@ namespace Clients; public class OllamaClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); - private readonly Model _embeddingModel = cacheModels.GetModel(options.Value.EmbeddingsModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( @@ -190,6 +192,8 @@ AsyncPolicyWrap combinedPolicy await ValidateEmbeddingMaxInputToken(string.Concat(inputs), path); ValidateRequestSizeAndContent(string.Concat(inputs)); + ArgumentNullException.ThrowIfNull(_embeddingModel); + // https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings var requestBody = new { diff --git a/src/Clients/OpenAiClient.cs b/src/Clients/OpenAiClient.cs index 3ffabde..710bf1a 100644 --- a/src/Clients/OpenAiClient.cs +++ b/src/Clients/OpenAiClient.cs @@ -23,14 +23,16 @@ namespace Clients; public class OpenAiClient( IHttpClientFactory httpClientFactory, - IOptions options, + IOptions llmOptions, ICacheModels cacheModels, ITokenizer tokenizer, AsyncPolicyWrap combinedPolicy ) : ILLMClient { - private readonly Model _chatModel = cacheModels.GetModel(options.Value.ChatModel); - private readonly Model _embeddingModel = cacheModels.GetModel(options.Value.EmbeddingsModel); + private readonly Model _chatModel = + cacheModels.GetModel(llmOptions.Value.ChatModel) + ?? throw new KeyNotFoundException($"Model '{llmOptions.Value.ChatModel}' not found in the ModelCache."); + private readonly Model? _embeddingModel = cacheModels.GetModel(llmOptions.Value.EmbeddingsModel); private const int MaxRequestSizeInBytes = 100 * 1024; // 100KB public async Task GetCompletionAsync( @@ -212,6 +214,8 @@ AsyncPolicyWrap combinedPolicy await ValidateEmbeddingMaxInputToken(string.Concat(inputs), path); ValidateRequestSizeAndContent(string.Concat(inputs)); + ArgumentNullException.ThrowIfNull(_embeddingModel); + var requestBody = new { input = inputs, diff --git a/src/Clients/Options/LLMOptions.cs b/src/Clients/Options/LLMOptions.cs index 57bafe4..632494e 100644 --- a/src/Clients/Options/LLMOptions.cs +++ b/src/Clients/Options/LLMOptions.cs @@ -1,7 +1,13 @@ +using Clients.Models; + namespace Clients.Options; public class LLMOptions { public string ChatModel { get; set; } = default!; - public string EmbeddingsModel { get; set; } = default!; + public string? EmbeddingsModel { get; set; } + public CodeAssistType? CodeAssistType { get; set; } + public CodeDiffType? CodeDiffType { get; set; } + public decimal? Temperature { get; set; } + public decimal? Threshold { get; set; } } diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj index afeb675..8b9e4bb 100644 --- a/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj +++ b/tests/UnitTests/BuildingBlocks.UnitTests/BuildingBlocks.UnitTests.csproj @@ -7,71 +7,9 @@ + - - - TestData\Calculator\.gitignore - Always - - - Always - - - - Always - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - Always - - - - - diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs b/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs new file mode 100644 index 0000000..3b2b8ec --- /dev/null +++ b/tests/UnitTests/BuildingBlocks.UnitTests/SpectreConsole/Markdown/SpectreMarkdownTests.cs @@ -0,0 +1,34 @@ +using BuildingBlocks.SpectreConsole.Markdown; +using Spectre.Console; +using Xunit; + +namespace BuildingBlocks.UnitTests.SpectreConsole.Markdown; + +public class SpectreMarkdownTests +{ + [Fact] + public void Test() + { + var s = new SpectreMarkdown( + @"1. **Calculate()**: + - **Description**: Calculates the sum of two numbers and updates the result field. + - **Returns**: The result of the addition as a double. +2. **AddNumbers(double first, double second)**: + - **Description**: A private method that adds two numbers. + - **Returns**: The sum of the two numbers." + ); + AnsiConsole.Write(s); + } + + [Fact] + public void Test2() + { + var s = new SpectreMarkdown( + @"Here are the method names inside the `Add` class: + +- `Calculate()` +- `AddNumbers(double first, double second)`" + ); + AnsiConsole.Write(s); + } +} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Calculator.csproj b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Calculator.csproj deleted file mode 100644 index 2150e37..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Calculator.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/IOperation.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/IOperation.cs deleted file mode 100644 index 81e3384..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/IOperation.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator; - -public interface IOperation -{ - double Calculate(); -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Add.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Add.cs deleted file mode 100644 index 568b0f4..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Add.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Add two value -/// -/// -/// -public class Add(double number1, double number2) : IOperation -{ - public double Calculate() - { - return AddNumbers(); - } - - private double AddNumbers() - { - return number1 / number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Divide.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Divide.cs deleted file mode 100644 index 1f2f8c1..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Divide.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Divide to values -/// -/// -/// -public class Divide(double number1, double number2) : IOperation -{ - public double Calculate() - { - return DivideNumbers(); - } - - private double DivideNumbers() - { - if (number1 == 0) - { - throw new DivideByZeroException("Cannot divide by zero."); - } - return number1 / number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Multiply.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Multiply.cs deleted file mode 100644 index 1795772..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Multiply.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Multiply two values -/// -/// -/// -public class Multiply(double number1, double number2) : IOperation -{ - public double Calculate() - { - return number1 * number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Subtract.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Subtract.cs deleted file mode 100644 index 871d99e..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Models/Subtract.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace BuildingBlocks.UnitTests.TestData.Calculator.Models; - -/// -/// Subtract two values -/// -/// -/// -public class Subtract(double number1, double number2) : IOperation -{ - public double Calculate() - { - return number1 - number2; - } -} diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Program.cs b/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Program.cs deleted file mode 100644 index 23bfea6..0000000 --- a/tests/UnitTests/BuildingBlocks.UnitTests/TestData/Calculator/Program.cs +++ /dev/null @@ -1,28 +0,0 @@ -using BuildingBlocks.UnitTests.TestData.Calculator; -using BuildingBlocks.UnitTests.TestData.Calculator.Models; - -Console.WriteLine("Simple Calculator\n"); - -// Input first number -Console.Write("Enter the first number: "); -double num1 = Convert.ToDouble(Console.ReadLine()); - -// Input operator -Console.Write("Enter an operation (+, -, *, /): "); -char operation = Convert.ToChar(Console.ReadLine()); - -// Input second number -Console.Write("Enter the second number: "); -double num2 = Convert.ToDouble(Console.ReadLine()); - -IOperation calculation = operation switch -{ - '+' => new Add(num1, num2), - '-' => new Subtract(num1, num2), - '*' => new Multiply(num1, num2), - '/' => new Divide(num1, num2), - _ => throw new InvalidOperationException("Invalid operation"), -}; - -double result = calculation.Calculate(); -Console.WriteLine($"Result: {result}"); diff --git a/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs b/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs index 4385ef8..22cb506 100644 --- a/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs +++ b/tests/UnitTests/BuildingBlocks.UnitTests/Utilities/FilesUtilitiesTests.cs @@ -43,210 +43,210 @@ public Task DisposeAsync() return Task.CompletedTask; } - [Theory] - [InlineData("bin/test.txt", true)] // Should be ignored - [InlineData("bin", true)] // Should be ignored - [InlineData("obj/test.txt", true)] // Should be ignored - [InlineData("src/main.cs", false)] // Should not be ignored - [InlineData("docs/readme.md", false)] // Should not be ignored - public void Test_IgnoredDirectoriesAndFiles(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("bin", true)] // Normal case - [InlineData("BIN", true)] // Uppercase - [InlineData("Bin", true)] // Mixed case - [InlineData("BiN/test.dll", true)] // Mixed case with file - [InlineData("bin/test.dll", true)] // Normal case with file - [InlineData("src/main.cs", false)] // Source directory, not ignored - public void Test_IgnoredBinWithDifferentCase(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("obj", true)] // The obj directory itself should be ignored - [InlineData("obj/Debug/test.dll", true)] // Files inside obj directory should be ignored - [InlineData("obj/Release/test.dll", true)] // Files inside obj directory should be ignored - [InlineData("src/main.cs", false)] // Should not be ignored (valid source folder) - [InlineData("docs/readme.md", false)] // Should not be ignored (documentation folder) - public void Test_IgnoredObjDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("obj", true)] // Normal case - [InlineData("OBJ", true)] // Uppercase - [InlineData("Obj", true)] // Mixed case - [InlineData("oBj/Debug/test.dll", true)] // Mixed case with a file path inside obj - [InlineData("obj/test.dll", true)] // Normal case with a file inside obj - [InlineData("src/main.cs", false)] // Source directory, should not be ignored - [InlineData("docs/readme.md", false)] // Documentation file, should not be ignored - public void Test_IgnoredObjWithDifferentCase(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("bin", true)] // Root bin directory - [InlineData("bin/", true)] // Explicit directory pattern with a trailing slash - [InlineData("bin/test.dll", true)] // File inside the bin directory - [InlineData("bin/Debug/test.dll", true)] // File inside a subdirectory of bin - [InlineData("src/bin/test.dll", false)] // A bin directory inside another path, should not be ignored - [InlineData("binExtra/test.dll", false)] // Similar name but different, should not be ignored - public void Test_IgnoredBinPatternFromGitignore(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("src/main.cs", false)] // Source file outside of bin - [InlineData("docs/readme.md", false)] // Documentation file outside of bin - [InlineData("assets/image.png", false)] // Image file outside of bin - [InlineData("config/settings.json", false)] // Configuration file outside of bin - [InlineData("binTest/test.dll", false)] // Directory similar to bin but should not be ignored - [InlineData("README.md", false)] // Project root file - public void Test_NotIgnoredFileOutsideBinDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("node_modules", true)] // The node_modules directory itself should be ignored - [InlineData("node_modules/", true)] // Explicit directory pattern with a trailing slash - [InlineData("node_modules/package.json", true)] // A file inside the node_modules directory - [InlineData("node_modules/react/index.js", true)] // A file in a subdirectory of node_modules - [InlineData("src/node_modules/test.js", false)] // A node_modules directory inside another path, should not be ignored - [InlineData("README.md", false)] // A file in the root that should not be ignored - public void Test_IgnoredNodeModulesDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("temp", true)] // The temp directory itself should be ignored - [InlineData("temp/", true)] // Explicit directory pattern with a trailing slash - [InlineData("temp/file.tmp", true)] // A file inside the temp directory - [InlineData("temp/session/anotherfile.tmp", true)] // A file in a subdirectory of temp - [InlineData("src/temp/test.tmp", false)] // A temp directory inside another path, should not be ignored - [InlineData("tempFiles/somefile.txt", false)] // A similar but different directory, should not be ignored - [InlineData("README.md", false)] // A file in the root that should not be ignored - public void Test_IgnoredTempDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("Models/Add.cs", false)] // Checking Add.cs file in Models folder - [InlineData("Models/Subtract.cs", false)] // Checking Subtract.cs file in Models folder - [InlineData("Models/Multiply.cs", false)] // Checking Multiply.cs file in Models folder - [InlineData("Models/Divide.cs", false)] // Checking Divide.cs file in Models folder - [InlineData("Program.cs", false)] // Checking Program.cs file in the root - [InlineData("Calculator.csproj", false)] // Checking Calculator.csproj in the root - public void Test_NotIgnoredFileInValidDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("logs", true)] // The logs directory itself should be ignored - [InlineData("logs/", true)] // Explicit directory pattern with a trailing slash - [InlineData("logs/application.log", true)] // A log file inside the logs directory - [InlineData("logs/2024-10-09.log", true)] // Another log file with a date pattern - [InlineData("src/logs/debug.log", false)] // A logs directory inside another path (should not be ignored) - [InlineData("temporaryLogs/test.log", false)] // A different directory that should not be ignored - [InlineData("README.md", false)] // A file that should not be ignored - public void Test_IgnoredLogsDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData(".env", true)] // The root .env file should be ignored - [InlineData(".env.example", false)] // An example env file, should not be ignored - [InlineData("config/.env", true)] // A .env file inside a config directory should be ignored - [InlineData("src/.env", true)] // Another .env file in a source folder - [InlineData("data/.env.backup", false)] // A backup file that should not be ignored - [InlineData("temp/.env.tmp", true)] // A temporary .env file that should be ignored - [InlineData("README.md", false)] // A file that is not a .env file and should not be ignored - public void Test_IgnoredDotEnvFile(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData(".keep", false)] - [InlineData("src/.keep", false)] - public void Test_NotIgnoredFileWithAllowedPrefix(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } - - [Theory] - [InlineData("cache", true)] // The cache directory itself should be ignored - [InlineData("cache/", true)] // Explicit directory pattern with a trailing slash - [InlineData("cache/temp.txt", true)] // A file inside the cache directory should be ignored - [InlineData("cache/images/image1.png", true)] // An image file inside a cache subdirectory should be ignored - [InlineData("src/cache/temp.log", false)] // A cache directory inside src should not be ignored - [InlineData("temporaryCache/test.txt", false)] // A different directory that should not be ignored - [InlineData("README.md", false)] // A file that is not in any cache directory and should not be ignored - public void Test_IgnoredCacheDirectory(string path, bool expected) - { - // Act - bool isIgnored = FilesUtilities.IsIgnored(path); - - // Assert - isIgnored.Should().Be(expected); - } + // [Theory] + // [InlineData("bin/test.txt", true)] // Should be ignored + // [InlineData("bin", true)] // Should be ignored + // [InlineData("obj/test.txt", true)] // Should be ignored + // [InlineData("src/main.cs", false)] // Should not be ignored + // [InlineData("docs/readme.md", false)] // Should not be ignored + // public void Test_IgnoredDirectoriesAndFiles(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("bin", true)] // Normal case + // [InlineData("BIN", true)] // Uppercase + // [InlineData("Bin", true)] // Mixed case + // [InlineData("BiN/test.dll", true)] // Mixed case with file + // [InlineData("bin/test.dll", true)] // Normal case with file + // [InlineData("src/main.cs", false)] // Source directory, not ignored + // public void Test_IgnoredBinWithDifferentCase(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("obj", true)] // The obj directory itself should be ignored + // [InlineData("obj/Debug/test.dll", true)] // Files inside obj directory should be ignored + // [InlineData("obj/Release/test.dll", true)] // Files inside obj directory should be ignored + // [InlineData("src/main.cs", false)] // Should not be ignored (valid source folder) + // [InlineData("docs/readme.md", false)] // Should not be ignored (documentation folder) + // public void Test_IgnoredObjDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("obj", true)] // Normal case + // [InlineData("OBJ", true)] // Uppercase + // [InlineData("Obj", true)] // Mixed case + // [InlineData("oBj/Debug/test.dll", true)] // Mixed case with a file path inside obj + // [InlineData("obj/test.dll", true)] // Normal case with a file inside obj + // [InlineData("src/main.cs", false)] // Source directory, should not be ignored + // [InlineData("docs/readme.md", false)] // Documentation file, should not be ignored + // public void Test_IgnoredObjWithDifferentCase(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("bin", true)] // Root bin directory + // [InlineData("bin/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("bin/test.dll", true)] // File inside the bin directory + // [InlineData("bin/Debug/test.dll", true)] // File inside a subdirectory of bin + // [InlineData("src/bin/test.dll", false)] // A bin directory inside another path, should not be ignored + // [InlineData("binExtra/test.dll", false)] // Similar name but different, should not be ignored + // public void Test_IgnoredBinPatternFromGitignore(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("src/main.cs", false)] // Source file outside of bin + // [InlineData("docs/readme.md", false)] // Documentation file outside of bin + // [InlineData("assets/image.png", false)] // Image file outside of bin + // [InlineData("config/settings.json", false)] // Configuration file outside of bin + // [InlineData("binTest/test.dll", false)] // Directory similar to bin but should not be ignored + // [InlineData("README.md", false)] // Project root file + // public void Test_NotIgnoredFileOutsideBinDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("node_modules", true)] // The node_modules directory itself should be ignored + // [InlineData("node_modules/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("node_modules/package.json", true)] // A file inside the node_modules directory + // [InlineData("node_modules/react/index.js", true)] // A file in a subdirectory of node_modules + // [InlineData("src/node_modules/test.js", false)] // A node_modules directory inside another path, should not be ignored + // [InlineData("README.md", false)] // A file in the root that should not be ignored + // public void Test_IgnoredNodeModulesDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("temp", true)] // The temp directory itself should be ignored + // [InlineData("temp/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("temp/file.tmp", true)] // A file inside the temp directory + // [InlineData("temp/session/anotherfile.tmp", true)] // A file in a subdirectory of temp + // [InlineData("src/temp/test.tmp", false)] // A temp directory inside another path, should not be ignored + // [InlineData("tempFiles/somefile.txt", false)] // A similar but different directory, should not be ignored + // [InlineData("README.md", false)] // A file in the root that should not be ignored + // public void Test_IgnoredTempDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("Models/Add.cs", false)] // Checking Add.cs file in Models folder + // [InlineData("Models/Subtract.cs", false)] // Checking Subtract.cs file in Models folder + // [InlineData("Models/Multiply.cs", false)] // Checking Multiply.cs file in Models folder + // [InlineData("Models/Divide.cs", false)] // Checking Divide.cs file in Models folder + // [InlineData("Program.cs", false)] // Checking Program.cs file in the root + // [InlineData("Calculator.csproj", false)] // Checking Calculator.csproj in the root + // public void Test_NotIgnoredFileInValidDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("logs", true)] // The logs directory itself should be ignored + // [InlineData("logs/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("logs/application.log", true)] // A log file inside the logs directory + // [InlineData("logs/2024-10-09.log", true)] // Another log file with a date pattern + // [InlineData("src/logs/debug.log", false)] // A logs directory inside another path (should not be ignored) + // [InlineData("temporaryLogs/test.log", false)] // A different directory that should not be ignored + // [InlineData("README.md", false)] // A file that should not be ignored + // public void Test_IgnoredLogsDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData(".env", true)] // The root .env file should be ignored + // [InlineData(".env.example", false)] // An example env file, should not be ignored + // [InlineData("config/.env", true)] // A .env file inside a config directory should be ignored + // [InlineData("src/.env", true)] // Another .env file in a source folder + // [InlineData("data/.env.backup", false)] // A backup file that should not be ignored + // [InlineData("temp/.env.tmp", true)] // A temporary .env file that should be ignored + // [InlineData("README.md", false)] // A file that is not a .env file and should not be ignored + // public void Test_IgnoredDotEnvFile(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData(".keep", false)] + // [InlineData("src/.keep", false)] + // public void Test_NotIgnoredFileWithAllowedPrefix(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } + // + // [Theory] + // [InlineData("cache", true)] // The cache directory itself should be ignored + // [InlineData("cache/", true)] // Explicit directory pattern with a trailing slash + // [InlineData("cache/temp.txt", true)] // A file inside the cache directory should be ignored + // [InlineData("cache/images/image1.png", true)] // An image file inside a cache subdirectory should be ignored + // [InlineData("src/cache/temp.log", false)] // A cache directory inside src should not be ignored + // [InlineData("temporaryCache/test.txt", false)] // A different directory that should not be ignored + // [InlineData("README.md", false)] // A file that is not in any cache directory and should not be ignored + // public void Test_IgnoredCacheDirectory(string path, bool expected) + // { + // // Act + // bool isIgnored = FilesUtilities.IsIgnored(path); + // + // // Assert + // isIgnored.Should().Be(expected); + // } } diff --git a/version.json b/version.json index 33f7ba2..9c870d9 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.0.0-preview", + "version": "1.0.0-preview2", "gitCommitIdShortAutoMinimum": 7, "nugetPackageVersion": { "semVer": 2