├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── BUG.yml │ └── FEATURE.yml ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── codecov.yml ├── dependabot.yml ├── readme_assets │ ├── csharprepl.mp4 │ ├── csharprepl.png │ ├── nuget.png │ └── vscode.png └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CSharpRepl.Services ├── CSharpRepl.Services.csproj ├── Completion │ ├── AutoCompleteService.cs │ └── OpenAICompleteService.cs ├── Configuration.cs ├── Disassembly │ └── Disassembler.cs ├── Dotnet │ └── DotnetBuilder.cs ├── Extensions │ ├── KeyExtensions.cs │ ├── LinqExtensions.cs │ ├── MiscExtensions.cs │ ├── RoslynExtensions.cs │ └── SpectreExtensions.cs ├── GlobalSuppressions.cs ├── IConsoleEx.cs ├── Logging │ └── ITraceLogger.cs ├── Nuget │ ├── ConsoleNugetLogger.cs │ ├── NugetHelper.cs │ └── NugetPackageInstaller.cs ├── Roslyn │ ├── DocumentationComment.cs │ ├── DocumentationCommentXmlNames.cs │ ├── Formatting │ │ ├── CustomObjectFormatters │ │ │ ├── CustomObjectFormatter.cs │ │ │ ├── GuidFormatter.cs │ │ │ ├── IEnumerableFormatter.cs │ │ │ ├── KeyValuePairFormatter.cs │ │ │ ├── MethodInfoFormatter.cs │ │ │ ├── ReflectionHelpers.cs │ │ │ ├── TupleFormatter.cs │ │ │ └── TypeFormatter.cs │ │ ├── FormattedObject.cs │ │ ├── LengthLimiting.cs │ │ ├── Level.cs │ │ ├── PrettyPrinter.Exceptions.cs │ │ ├── PrettyPrinter.Members.cs │ │ ├── PrettyPrinter.cs │ │ ├── PrimitiveFormatter.cs │ │ ├── Rendering │ │ │ ├── FormattedObjectRenderable.cs │ │ │ └── RenderableSequence.cs │ │ └── TypeNameFormatter.cs │ ├── MetadataResolvers │ │ ├── AlternativeReferenceResolver.cs │ │ ├── AssemblyReferenceMetadataResolver.cs │ │ ├── CompositeAlternativeReferenceResolver.cs │ │ ├── CompositeMetadataReferenceResolver.cs │ │ ├── NugetPackageMetadataResolver.cs │ │ └── SolutionFileMetadataResolver.cs │ ├── Microsoft.CodeAnalysis.CSharp.Symbols │ │ ├── GeneratedNameConstants.cs │ │ ├── GeneratedNameKind.cs │ │ ├── GeneratedNameParser.cs │ │ └── GeneratedNames.cs │ ├── Microsoft.CodeAnalysis.CSharp │ │ └── ObjectDisplay.cs │ ├── Microsoft.CodeAnalysis.PooledObjects │ │ ├── ArrayBuilder.Enumerator.cs │ │ ├── ArrayBuilder.cs │ │ ├── ObjectPool`1.cs │ │ ├── PooledHashSet.cs │ │ └── PooledStringBuilder.cs │ ├── Microsoft.CodeAnalysis.Scripting.Hosting │ │ ├── MemberFilter.cs │ │ ├── ObjectFormatterHelpers.cs │ │ ├── PrimitiveFormatterOptions.cs │ │ └── TypeNameFormatterOptions.cs │ ├── Microsoft.CodeAnalysis │ │ ├── ObjectDisplayExtensions.cs │ │ └── ObjectDisplayOptions.cs │ ├── OverloadItemGenerator.cs │ ├── References │ │ ├── AssemblyReferenceComparer.cs │ │ ├── AssemblyReferenceService.cs │ │ ├── DotNetInstallationLocator.cs │ │ └── SharedFramework.cs │ ├── RoslynServices.Overloads.cs │ ├── RoslynServices.cs │ ├── Scripting │ │ ├── EvaluationResult.cs │ │ ├── ScriptGlobals.cs │ │ └── ScriptRunner.cs │ ├── WorkspaceManager.cs │ └── XmlFragmentParser.cs ├── RuntimeHelper.cs ├── SymbolExploration │ ├── DebugSymbolLoader.cs │ ├── NullSymbolLogger.cs │ ├── SourceLink │ │ ├── GitHubSourceLinkHost.cs │ │ └── SourceLinkLookup.cs │ └── SymbolExplorer.cs ├── SyntaxHighlighting │ ├── HighlightedSpan.cs │ └── SyntaxHighlighter.cs ├── SystemConsoleEx.cs ├── Theming │ ├── ExportVisualStudioTheme.csx │ ├── FormattedStringParser.cs │ ├── StyledString.cs │ ├── StyledStringBuilder.cs │ ├── StyledStringSegment.cs │ ├── SyntaxHighlightingColor.cs │ ├── Theme.cs │ └── ThemeColor.cs └── runtime.json ├── CSharpRepl.Tests ├── CSharpRepl.Tests.csproj ├── CommandLineTests.cs ├── CompletionTests.cs ├── ConfigurationTests.cs ├── Data │ ├── ComplexSolution │ │ ├── ComplexSolution.sln │ │ ├── EntryPoint │ │ │ ├── EntryPoint.csproj │ │ │ └── Program.cs │ │ ├── LibraryA │ │ │ ├── A.cs │ │ │ └── LibraryA.csproj │ │ └── LibraryB │ │ │ ├── B.cs │ │ │ └── LibraryB.csproj │ ├── Config.rsp │ ├── DemoLibrary.README.txt │ ├── DemoLibrary.dll │ ├── DemoSolution │ │ ├── DemoSolution.DemoProject1 │ │ │ ├── DemoClass1.cs │ │ │ └── DemoSolution.DemoProject1.csproj │ │ ├── DemoSolution.DemoProject2 │ │ │ ├── DemoClass2.cs │ │ │ └── DemoSolution.DemoProject2.csproj │ │ ├── DemoSolution.DemoProject3 │ │ │ ├── DemoClass3.cs │ │ │ └── DemoSolution.DemoProject3.csproj │ │ └── DemoSolution.sln │ ├── Disassembly │ │ ├── TopLevelProgram.Input.txt │ │ ├── TopLevelProgram.Output.Debug.il │ │ ├── TopLevelProgram.Output.Release.il │ │ ├── TypeDeclaration.Input.txt │ │ ├── TypeDeclaration.Output.Debug.il │ │ └── TypeDeclaration.Output.Release.il │ ├── LoadScript.csx │ ├── ResponseFile.rsp │ ├── WebApplication1.dll │ ├── WebApplication1.runtimeconfig.json │ └── theme.json ├── DisassemblerTests.cs ├── DotNetInstallationLocatorTest.cs ├── EvaluationTests.cs ├── Extensions.cs ├── FakeConsole.cs ├── FakeHttp.cs ├── FormattedStringParserTests.cs ├── NugetPackageInstallerTests.cs ├── ObjectFormatting │ ├── CustomObjectFormattersTests.cs │ ├── PrettyPrinterTests.cs │ └── TestFormatter.cs ├── PipedInputEvaluatorTests.cs ├── ProgramTests.cs ├── PromptConfigurationTests.cs ├── ReadEvalPrintLoopTests.cs ├── RoslynServicesFixture.cs ├── RoslynServicesTests.Overloads.cs ├── RoslynServicesTests.cs ├── ScriptArgumentTests.cs ├── StyledStringTests.cs ├── SymbolExplorerTests.cs ├── SyntaxHighlightingTests.cs └── TraceLoggerTests.cs ├── CSharpRepl.sln ├── CSharpRepl.sln.licenseheader ├── CSharpRepl ├── CSharpRepl.csproj ├── CSharpReplPromptCallbacks.cs ├── CommandLine.cs ├── ConfigurationFile.cs ├── Logging │ ├── NullLogger.cs │ └── TraceLogger.cs ├── PipedInputEvaluator.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── ReadEvalPrintLoop.cs └── themes │ ├── VisualStudio_BlueExtraContrast.json │ ├── VisualStudio_Dark.json │ ├── VisualStudio_Light.json │ └── dracula.json ├── LICENSE.txt ├── README.md ├── nuget.config └── publish-release.ps1 /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | indent_style=space 4 | end_of_line=crlf 5 | 6 | # CS1591: Missing XML comment for publicly visible type or member 7 | dotnet_diagnostic.CS1591.severity = none 8 | 9 | # S3358: Ternary operators should not be nested 10 | dotnet_diagnostic.S3358.severity = none 11 | 12 | csharp_style_namespace_declarations=file_scoped:warning 13 | 14 | # CA1068: CancellationToken parameters must come last 15 | dotnet_diagnostic.CA1068.severity = warning 16 | 17 | # CA2250: Use ThrowIfCancellationRequested 18 | dotnet_diagnostic.CA2250.severity = warning 19 | 20 | # CA2016: Forward the CancellationToken parameter to methods that take one 21 | dotnet_diagnostic.CA2016.severity = warning 22 | 23 | # IDE0040: Add accessibility modifiers 24 | dotnet_diagnostic.IDE0040.severity = warning -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # We'll let Git's auto-detection algorithm infer if a file is text. If it is, 2 | # enforce CRLF line endings regardless of OS or git configurations. 3 | * text=auto eol=crlf 4 | 5 | # Isolate binary files in case the auto-detection algorithm fails and 6 | # marks them as text files (which could brick them). 7 | *.{png,jpg,jpeg,gif,webp,woff,woff2} binary -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: [bug] 4 | assignees: 5 | - waf 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: input 12 | id: version 13 | attributes: 14 | label: Version 15 | description: What version of `csharprepl` are you running? You can check with `csharprepl -v` 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: what-happened 20 | attributes: 21 | label: What happened? 22 | description: If you found a crash, please include the output of `csharprepl --trace` in your report. 23 | placeholder: Tell us what you see. 24 | value: "Something went wrong :(" 25 | validations: 26 | required: true 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE.yml: -------------------------------------------------------------------------------- 1 | name: Feature 2 | description: Request or design a new feature 3 | labels: [enhancement] 4 | assignees: 5 | - waf 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to discuss this feature! 11 | - type: textarea 12 | id: feature-description 13 | attributes: 14 | label: Feature Description 15 | description: Does this work in other REPLs, and is it compatible across Mac/Linux/Windows? 16 | validations: 17 | required: true 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thanks for creating a PR! Please make sure you've done the following (replace [ ] with [X]): 2 | 3 | From the [Contributing Guidelines](https://github.com/waf/CSharpRepl/blob/main/README.md#contributing): 4 | 5 | - [ ] If adding a new feature, I discussed it's design in an issue first 6 | - [ ] I verified there are no code warnings emitted for my code 7 | - [ ] I've included a unit test, and all the unit tests are passing 8 | 9 | **Pull Request Description** 10 | 11 | Your text goes here -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 60..80 # test coverage thresholds. 60% and below fails (red), 80% and up passes (green), between is "yellow" 3 | round: down 4 | precision: 1 5 | status: 6 | project: 7 | default: 8 | target: 70% # require new commits to have at least 70% coverage 9 | threshold: 2% # more than 2% coverage drop, fail. 10 | patch: 11 | default: 12 | enabled: no 13 | ignore: 14 | - "**/Microsoft.CodeAnalysis.CSharp.Symbols" # ignore folders and all its contents 15 | - "**/Microsoft.CodeAnalysis.PooledObjects" # ignore folders and all its contents 16 | - "**/Microsoft.CodeAnalysis" # ignore folders and all its contents -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: nuget 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | groups: 10 | dotnet-dependencies: 11 | patterns: 12 | - "*" 13 | -------------------------------------------------------------------------------- /.github/readme_assets/csharprepl.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waf/CSharpRepl/bd79130d49c06736a2d5f4d56ac7643889ad2328/.github/readme_assets/csharprepl.mp4 -------------------------------------------------------------------------------- /.github/readme_assets/csharprepl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waf/CSharpRepl/bd79130d49c06736a2d5f4d56ac7643889ad2328/.github/readme_assets/csharprepl.png -------------------------------------------------------------------------------- /.github/readme_assets/nuget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waf/CSharpRepl/bd79130d49c06736a2d5f4d56ac7643889ad2328/.github/readme_assets/nuget.png -------------------------------------------------------------------------------- /.github/readme_assets/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waf/CSharpRepl/bd79130d49c06736a2d5f4d56ac7643889ad2328/.github/readme_assets/vscode.png -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | platform: [ubuntu-latest, windows-latest] 14 | 15 | runs-on: ${{ matrix.platform }} 16 | 17 | steps: 18 | - uses: actions/checkout@230611dbd0eb52da1e1f4f7bc8bb0c3a339fc8b7 19 | 20 | - name: Install Dotnet 21 | uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a 22 | with: 23 | dotnet-version: '9.0.x' 24 | 25 | - name: Dotnet Installation Info 26 | run: dotnet --info 27 | 28 | - name: Build 29 | run: dotnet build 30 | 31 | - name: Test 32 | run: dotnet test --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover 33 | 34 | - name: Report Code Coverage 35 | if: matrix.platform == 'windows-latest' # only generate and upload code coverage once 36 | uses: codecov/codecov-action@260aa3b4b2f265b8578bc0e721e33ebf8ff53313 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | files: CSharpRepl.Tests/coverage.opencover.xml 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # On pushing a tag like v0.3.2, publish to nuget.org 2 | # Use publish-release.ps1 to push a release 3 | name: release 4 | 5 | on: 6 | push: 7 | tags: 8 | - "v[0-9]+.*" # not a regex! ".*" means a period followed by any character https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: windows-latest 14 | 15 | steps: 16 | - uses: actions/checkout@230611dbd0eb52da1e1f4f7bc8bb0c3a339fc8b7 17 | 18 | - name: Install Dotnet 19 | uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a 20 | with: 21 | dotnet-version: '9.0.x' 22 | 23 | - name: Dotnet Installation Info 24 | run: dotnet --info 25 | 26 | - name: Pack 27 | run: dotnet pack -c Release /p:ContinuousIntegrationBuild=true 28 | 29 | # publish to nuget. This will publish both a nupkg and snupkg file. 30 | - name: Publish 31 | shell: pwsh 32 | run: | 33 | pwd 34 | cd CSharpRepl/nupkg 35 | ls 36 | dotnet nuget push CSharpRepl.*.nupkg --api-key=${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/CSharpRepl/bin/Debug/net5.0/CSharpRepl.dll", 13 | "args": [], 14 | "cwd": "${workspaceFolder}/CSharpRepl", 15 | "console": "integratedTerminal", 16 | "stopAtEntry": false 17 | }, 18 | { 19 | "name": ".NET Core Attach", 20 | "type": "coreclr", 21 | "request": "attach" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/CSharpRepl/CSharpRepl.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/CSharpRepl/CSharpRepl.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/CSharpRepl/CSharpRepl.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you'd like to help out, thanks! We use Visual Studio 2022 for development, though any standard .NET 5 development environment should work. Please read through these guidelines to get started: 4 | 5 | - Read through the ARCHITECTURE.md file to understand how csharprepl works. Depending on what you want to do, changes to the underlying PrettyPrompt library may be required. 6 | - For new features, please open an issue first to discuss and design the feature. This will help reduce the chance of conflicting designs. 7 | - Please include an xunit test, and ensure any code warnings and nullability issues are resolved. 8 | 9 | Thanks! 10 | 11 | ## CSharpRepl Contributors 12 | 13 | Thanks to everyone who contributes! The following contributors have helped out with CSharpRepl: 14 | 15 | - Hubert Kindermann ([kindermannhubert](https://github.com/kindermannhubert)) 16 | - [IBIT-ZEE](https://github.com/IBIT-ZEE) 17 | - Nattapong Nunpan ([aixasz](https://github.com/aixasz)) 18 | - Ivan Kara ([realivanjx](https://github.com/realivanjx)) 19 | - José Javier Rodríguez Zas (JJ) ([jjavierdguezas](https://github.com/jjavierdguezas)) 20 | - Marlon Regenhardt ([Regenhardt](https://github.com/Regenhardt)) 21 | - Luiz-Ossinho ([Luiz-Ossinho](https://github.com/Luiz-Ossinho)) 22 | - Vinod Pal ([VNDPAL](https://github.com/VNDPAL)) 23 | - Rasim Keita ([Keyros](https://github.com/Keyros)) 24 | - Atif Aziz ([atifaziz](https://github.com/atifaziz)) 25 | - [lonix1](https://github.com/lonix1) 26 | - Weihan Li ([WeihanLi](https://github.com/WeihanLi)) 27 | 28 | ## PrettyPrompt Contributors 29 | 30 | CSharpRepl heavily relies on the PrettyPrompt library, so make sure to check out the [contributors](https://github.com/waf/PrettyPrompt/blob/main/CONTRIBUTORS.md) there, too! 31 | -------------------------------------------------------------------------------- /CSharpRepl.Services/CSharpRepl.Services.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | PreserveNewest 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Completion/OpenAICompleteService.cs: -------------------------------------------------------------------------------- 1 | #region License Header 2 | // This Source Code Form is subject to the terms of the Mozilla Public 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | #endregion 6 | 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Net.Http; 11 | using System.Runtime.CompilerServices; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using OpenAI.Chat; 15 | 16 | namespace CSharpRepl.Services.Completion; 17 | 18 | /// 19 | /// Call the OpenAI API to get C# completions. Requires an OpenAI API token (which can be purchased from OpenAI). 20 | /// 21 | public class OpenAICompleteService 22 | { 23 | public const int DefaultHistoryEntryCount = 5; 24 | public const string DefaultModel = "gpt-4o"; 25 | public const string DefaultPrompt = 26 | "// Complete the following C# code that will be run in a REPL. Do not output markdown code fences like ```. " 27 | + "Prefer functions, statements, and expressions instead of a full program. Prefer modern, terse C# over more verbose C#. " 28 | + "Never comment what the code prints. Any plain-text, English answers MUST be in a C# comment, and C# code should not be inside comments."; 29 | public const string ApiKeyEnvironmentVariableName = "OPENAI_API_KEY"; 30 | 31 | private readonly ChatClient? client; // null if no Open AI API key is available. 32 | private readonly SystemChatMessage? prompt; 33 | 34 | public OpenAICompleteService(OpenAIConfiguration? configuration, ChatClient? chatClient = null) 35 | { 36 | if (configuration is null || string.IsNullOrEmpty(configuration.ApiKey)) 37 | { 38 | return; 39 | } 40 | 41 | client = chatClient ?? new ChatClient(configuration.Model, configuration.ApiKey); 42 | prompt = new SystemChatMessage(configuration.Prompt ?? DefaultPrompt); 43 | } 44 | 45 | public static string? ApiKey => 46 | Environment.GetEnvironmentVariable(ApiKeyEnvironmentVariableName, EnvironmentVariableTarget.Process) 47 | ?? Environment.GetEnvironmentVariable(ApiKeyEnvironmentVariableName, EnvironmentVariableTarget.User); 48 | 49 | public async IAsyncEnumerable CompleteAsync(IReadOnlyList submissions, string code, int caret, [EnumeratorCancellation] CancellationToken cancellationToken) 50 | { 51 | if (client is null) 52 | { 53 | yield break; 54 | } 55 | 56 | var input = new ChatMessage[] { prompt! } 57 | .Concat(submissions.Append(code).Select(s => new UserChatMessage(s))) 58 | .ToArray(); 59 | 60 | var output = client.CompleteChatStreamingAsync(input, cancellationToken: cancellationToken); 61 | 62 | await foreach (var update in output.WithCancellation(cancellationToken)) 63 | { 64 | if (update is null or { ContentUpdate.Count: 0 }) 65 | { 66 | continue; 67 | } 68 | var content = update.ContentUpdate[0].Text; 69 | if (string.IsNullOrEmpty(content)) 70 | { 71 | yield return "\n"; 72 | } 73 | yield return content.Replace("\t", " "); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Dotnet/DotnetBuilder.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using System.Diagnostics; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace CSharpRepl.Services.Dotnet; 13 | 14 | internal class DotnetBuilder 15 | { 16 | private readonly IConsoleEx console; 17 | 18 | public DotnetBuilder(IConsoleEx console) 19 | { 20 | this.console = console; 21 | } 22 | 23 | public (int exitCode, ImmutableArray outputLines) Build(string path) 24 | { 25 | using var process = StartBuild(path, out var output); 26 | process.WaitForExit(); 27 | return (process.ExitCode, output.ToImmutableArray()); 28 | } 29 | 30 | public async Task<(int exitCode, ImmutableArray outputLines)> BuildAsync(string path, CancellationToken cancellationToken) 31 | { 32 | using var process = StartBuild(path, out var output); 33 | await process.WaitForExitAsync(cancellationToken); 34 | return (process.ExitCode, output.ToImmutableArray()); 35 | } 36 | 37 | private Process StartBuild(string path, out List output) 38 | { 39 | output = []; 40 | var process = new Process 41 | { 42 | StartInfo = 43 | { 44 | FileName = OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet", 45 | ArgumentList = { "build", path }, 46 | RedirectStandardOutput = true 47 | } 48 | }; 49 | 50 | var outputForClosure = output; 51 | process.OutputDataReceived += (_, data) => 52 | { 53 | if (data.Data is null) return; 54 | 55 | outputForClosure.Add(data.Data); 56 | console.WriteLine(data.Data); 57 | }; 58 | 59 | console.WriteLine("Building " + path); 60 | process.Start(); 61 | process.BeginOutputReadLine(); 62 | return process; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Extensions/KeyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using PrettyPrompt.Consoles; 4 | 5 | namespace CSharpRepl.Services.Extensions; 6 | 7 | public static class KeyExtensions 8 | { 9 | public static string GetStringValue(this KeyPressPattern pattern) 10 | { 11 | if (pattern.Key != default) 12 | { 13 | return pattern.Modifiers == default 14 | ? $"{pattern.Key}" 15 | : $"{pattern.Modifiers.GetStringValue()}+{pattern.Key}"; 16 | } 17 | return $"{pattern.Character}"; 18 | } 19 | 20 | private static string GetStringValue(this ConsoleModifiers modifiers) 21 | { 22 | var values = new[] { ConsoleModifiers.Control, ConsoleModifiers.Alt, ConsoleModifiers.Shift } 23 | .Where(x => modifiers.HasFlag(x)) 24 | .OrderDescending() 25 | .Select(x => x.ToString()); 26 | return string.Join("+", values); 27 | } 28 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Extensions/LinqExtensions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace CSharpRepl.Services.Extensions; 9 | 10 | internal static class LinqExtensions 11 | { 12 | // purely for nullable reference type analysis 13 | public static IEnumerable WhereNotNull(this IEnumerable source) where T : class 14 | { 15 | return source.Where(x => x != null)!; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Extensions/MiscExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace CSharpRepl.Services.Extensions; 4 | 5 | internal static class MiscExtensions 6 | { 7 | public static bool TryGet(this T? nullableValue, out T value) 8 | where T : struct 9 | { 10 | if (nullableValue.HasValue) 11 | { 12 | value = nullableValue.GetValueOrDefault(); 13 | return true; 14 | } 15 | else 16 | { 17 | value = default; 18 | return false; 19 | } 20 | } 21 | 22 | public static bool TryGet(this T? nullableValue, [MaybeNullWhen(false)] out T value) 23 | where T : class 24 | { 25 | if (nullableValue is null) 26 | { 27 | value = null; 28 | return false; 29 | } 30 | else 31 | { 32 | value = nullableValue; 33 | return true; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Extensions/SpectreExtensions.cs: -------------------------------------------------------------------------------- 1 | using CSharpRepl.Services.Theming; 2 | using Spectre.Console; 3 | 4 | namespace CSharpRepl.Services.Extensions; 5 | 6 | internal static class SpectreExtensions 7 | { 8 | public static Paragraph Append(this Paragraph paragraph, StyledStringSegment text) 9 | => paragraph.Append(text.Text, text.Style); 10 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | // This file is used by Code Analysis to maintain SuppressMessage 6 | // attributes that are applied to this project. 7 | // Project-level suppressions either have no target or are given 8 | // a specific target and scoped to a namespace, type, member, etc. 9 | 10 | using System.Diagnostics.CodeAnalysis; 11 | 12 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static")] 13 | -------------------------------------------------------------------------------- /CSharpRepl.Services/IConsoleEx.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using PrettyPrompt.Consoles; 6 | using PrettyPrompt.Highlighting; 7 | using Spectre.Console; 8 | using Spectre.Console.Rendering; 9 | 10 | namespace CSharpRepl.Services; 11 | 12 | public interface IConsoleEx : IAnsiConsole 13 | { 14 | IConsole PrettyPromptConsole { get; } 15 | 16 | private IAnsiConsole AnsiConsole => this; 17 | 18 | void Write(string text) => AnsiConsole.Write(text); 19 | void Write(FormattedString text) => PrettyPromptConsole.Write(text); 20 | 21 | void WriteLine(string text) => AnsiConsole.WriteLine(text); 22 | void WriteLine() => AnsiConsole.WriteLine(); 23 | void WriteLine(FormattedString text) => PrettyPromptConsole.WriteLine(text); 24 | 25 | void WriteError(string text) 26 | { 27 | if (PrettyPromptConsole.IsErrorRedirected) 28 | { 29 | PrettyPromptConsole.WriteError(text); 30 | } 31 | else 32 | { 33 | //AnsiConsole is smarter about word wrapping 34 | Write(text); 35 | } 36 | } 37 | 38 | /// Text written to error stream (used only if error stream is redirected). 39 | void WriteError(IRenderable renderable, string text) 40 | { 41 | if (PrettyPromptConsole.IsErrorRedirected) 42 | { 43 | PrettyPromptConsole.WriteError(text); 44 | } 45 | else 46 | { 47 | //AnsiConsole is smarter about word wrapping 48 | Write(renderable); 49 | } 50 | } 51 | 52 | /// Text written to error stream (used only if error stream is redirected). 53 | void WriteErrorLine(IRenderable renderable, string text) 54 | { 55 | if (PrettyPromptConsole.IsErrorRedirected) 56 | { 57 | PrettyPromptConsole.WriteErrorLine(text); 58 | } 59 | else 60 | { 61 | //AnsiConsole is smarter about word wrapping 62 | Write(renderable); 63 | WriteLine(); 64 | } 65 | } 66 | 67 | void WriteErrorLine(string text) 68 | { 69 | if (PrettyPromptConsole.IsErrorRedirected) 70 | { 71 | PrettyPromptConsole.WriteErrorLine(text); 72 | } 73 | else 74 | { 75 | //AnsiConsole is smarter about word wrapping 76 | WriteLine(text); 77 | } 78 | } 79 | 80 | string? ReadLine(); 81 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Logging/ITraceLogger.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace CSharpRepl.Services.Logging; 9 | 10 | public interface ITraceLogger 11 | { 12 | void Log(string message); 13 | void Log(Func message); 14 | void LogPaths(string message, Func> paths); 15 | } 16 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Nuget/NugetHelper.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.IO; 8 | using System.Reflection; 9 | using System.Runtime.Versioning; 10 | using NuGet.Frameworks; 11 | using NuGet.RuntimeModel; 12 | 13 | namespace CSharpRepl.Services.Nuget; 14 | 15 | internal static class NugetHelper 16 | { 17 | private const string RuntimeFileName = "runtime.json"; 18 | 19 | public static RuntimeGraph GetRuntimeGraph(Action? error) 20 | { 21 | var dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 22 | if (dir != null) 23 | { 24 | var path = Path.Combine(dir, RuntimeFileName); 25 | if (File.Exists(path)) 26 | { 27 | using var stream = File.OpenRead(path); 28 | return JsonRuntimeFormat.ReadRuntimeGraph(stream); 29 | } 30 | } 31 | error?.Invoke($"Cannot find '{RuntimeFileName}' in '{dir}'"); 32 | return new RuntimeGraph(); 33 | } 34 | 35 | public static bool TryGetCurrentFramework([NotNullWhen(true)] out NuGetFramework? result) 36 | { 37 | var assembly = Assembly.GetEntryAssembly(); 38 | if (assembly is null || 39 | assembly.Location.EndsWith("testhost.dll", StringComparison.OrdinalIgnoreCase)) //for unit tests (testhost.dll targets netcoreapp2.1 instead of net6.0) 40 | { 41 | assembly = Assembly.GetExecutingAssembly(); 42 | } 43 | 44 | var targetFrameworkAttribute = assembly.GetCustomAttribute(); 45 | if (targetFrameworkAttribute is null) 46 | { 47 | result = null; 48 | return false; 49 | } 50 | 51 | result = NuGetFramework.Parse(targetFrameworkAttribute.FrameworkName); 52 | return true; 53 | } 54 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/DocumentationCommentXmlNames.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | //Modified copy of https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/InternalUtilities/DocumentationCommentXmlNames.cs 6 | 7 | using System; 8 | 9 | namespace CSharpRepl.Services.Roslyn; 10 | 11 | /// 12 | /// Names of well-known XML attributes and elements. 13 | /// 14 | internal static class DocumentationCommentXmlNames 15 | { 16 | public const string CElementName = "c"; 17 | public const string CodeElementName = "code"; 18 | public const string CompletionListElementName = "completionlist"; 19 | public const string DescriptionElementName = "description"; 20 | public const string ExampleElementName = "example"; 21 | public const string ExceptionElementName = "exception"; 22 | public const string IncludeElementName = "include"; 23 | public const string InheritdocElementName = "inheritdoc"; 24 | public const string ItemElementName = "item"; 25 | public const string ListElementName = "list"; 26 | public const string ListHeaderElementName = "listheader"; 27 | public const string ParaElementName = "para"; 28 | public const string ParameterElementName = "param"; 29 | public const string ParameterReferenceElementName = "paramref"; 30 | public const string PermissionElementName = "permission"; 31 | public const string PlaceholderElementName = "placeholder"; 32 | public const string PreliminaryElementName = "preliminary"; 33 | public const string RemarksElementName = "remarks"; 34 | public const string ReturnsElementName = "returns"; 35 | public const string SeeElementName = "see"; 36 | public const string SeeAlsoElementName = "seealso"; 37 | public const string SummaryElementName = "summary"; 38 | public const string TermElementName = "term"; 39 | public const string ThreadSafetyElementName = "threadsafety"; 40 | public const string TypeParameterElementName = "typeparam"; 41 | public const string TypeParameterReferenceElementName = "typeparamref"; 42 | public const string ValueElementName = "value"; 43 | 44 | public const string CrefAttributeName = "cref"; 45 | public const string HrefAttributeName = "href"; 46 | public const string FileAttributeName = "file"; 47 | public const string InstanceAttributeName = "instance"; 48 | public const string LangwordAttributeName = "langword"; 49 | public const string NameAttributeName = "name"; 50 | public const string PathAttributeName = "path"; 51 | public const string StaticAttributeName = "static"; 52 | public const string TypeAttributeName = "type"; 53 | 54 | public static bool ElementEquals(string name1, string name2, bool fromVb = false) 55 | { 56 | return string.Equals(name1, name2, fromVb ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); 57 | } 58 | 59 | public static bool AttributeEquals(string name1, string name2) 60 | { 61 | return string.Equals(name1, name2, StringComparison.Ordinal); 62 | } 63 | 64 | public static new bool Equals(object left, object right) 65 | { 66 | return object.Equals(left, right); 67 | } 68 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/CustomObjectFormatter.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using CSharpRepl.Services.Roslyn.Formatting.Rendering; 7 | using CSharpRepl.Services.SyntaxHighlighting; 8 | using CSharpRepl.Services.Theming; 9 | using Spectre.Console; 10 | 11 | namespace CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 12 | 13 | internal interface ICustomObjectFormatter 14 | { 15 | /// 16 | /// Is this formatter to the value? 17 | /// 18 | bool IsApplicable(object value); 19 | 20 | StyledString FormatToText(object value, Level level, Formatter formatter); 21 | FormattedObjectRenderable FormatToRenderable(object value, Level level, Formatter formatter); 22 | } 23 | 24 | internal abstract class CustomObjectFormatter : ICustomObjectFormatter 25 | { 26 | public virtual bool IsApplicable(object value) 27 | { 28 | if (value is null) return true; 29 | return value.GetType().IsAssignableTo(Type); 30 | } 31 | 32 | public abstract Type Type { get; } 33 | 34 | public abstract StyledString FormatToText(object value, Level level, Formatter formatter); 35 | 36 | public virtual FormattedObjectRenderable FormatToRenderable(object value, Level level, Formatter formatter) 37 | => new(FormatToText(value, level, formatter).ToParagraph(), renderOnNewLine: false); 38 | } 39 | 40 | internal abstract class CustomObjectFormatter : CustomObjectFormatter 41 | where T : notnull 42 | { 43 | public sealed override Type Type => typeof(T); 44 | 45 | public sealed override StyledString FormatToText(object value, Level level, Formatter formatter) 46 | => FormatToText((T)value, level, formatter); 47 | 48 | public sealed override FormattedObjectRenderable FormatToRenderable(object value, Level level, Formatter formatter) 49 | => FormatToRenderable((T)value, level, formatter); 50 | 51 | public abstract StyledString FormatToText(T value, Level level, Formatter formatter); 52 | 53 | public virtual FormattedObjectRenderable FormatToRenderable(T value, Level level, Formatter formatter) 54 | => base.FormatToRenderable(value, level, formatter); 55 | } 56 | 57 | internal class Formatter 58 | { 59 | private readonly PrettyPrinter prettyPrinter; 60 | private readonly SyntaxHighlighter syntaxHighlighter; 61 | 62 | public StyledStringSegment NullLiteral => prettyPrinter.NullLiteral; 63 | public Style KeywordStyle => syntaxHighlighter.KeywordStyle; 64 | public Profile ConsoleProfile { get; } 65 | 66 | public Formatter(PrettyPrinter prettyPrinter, SyntaxHighlighter syntaxHighlighter, Profile consoleProfile) 67 | { 68 | this.prettyPrinter = prettyPrinter; 69 | this.syntaxHighlighter = syntaxHighlighter; 70 | ConsoleProfile = consoleProfile; 71 | } 72 | 73 | public StyledString FormatObjectToText(object? obj, Level level) 74 | => prettyPrinter.FormatObjectToText(obj, level); 75 | 76 | public FormattedObjectRenderable FormatObjectToRenderable(object? obj, Level level) 77 | => prettyPrinter.FormatObjectToRenderable(obj, level); 78 | 79 | public StyledString FormatTypeName(Type type, bool showNamespaces, bool useLanguageKeywords, bool hideSystemNamespace) 80 | => prettyPrinter.FormatTypeName(type, showNamespaces, useLanguageKeywords, hideSystemNamespace); 81 | 82 | public Style GetStyle(string? classification) 83 | => syntaxHighlighter.GetStyle(classification); 84 | 85 | public StyledString GetValueRetrievalExceptionText(Exception exception, Level level) 86 | => prettyPrinter.GetValueRetrievalExceptionText(exception, level); 87 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/GuidFormatter.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using CSharpRepl.Services.Theming; 7 | using Microsoft.CodeAnalysis.Classification; 8 | 9 | namespace CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 10 | 11 | internal sealed class GuidFormatter : CustomObjectFormatter 12 | { 13 | public static readonly GuidFormatter Instance = new(); 14 | 15 | private GuidFormatter() { } 16 | 17 | public override StyledString FormatToText(Guid value, Level level, Formatter formatter) 18 | { 19 | //32 digits separated by hyphens: 00000000-0000-0000-0000-000000000000 20 | var parts = value.ToString().Split('-'); 21 | var style = formatter.GetStyle(ClassificationTypeNames.NumericLiteral); 22 | return new StyledString( 23 | [ 24 | new(parts[0], style), 25 | new("-"), 26 | new(parts[1], style), 27 | new("-"), 28 | new(parts[2], style), 29 | new("-"), 30 | new(parts[3], style), 31 | new("-"), 32 | new(parts[4], style) 33 | ]); 34 | } 35 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/KeyValuePairFormatter.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using CSharpRepl.Services.Theming; 8 | 9 | namespace CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 10 | 11 | internal sealed class KeyValuePairFormatter : CustomObjectFormatter 12 | { 13 | public static readonly KeyValuePairFormatter Instance = new(); 14 | 15 | public override Type Type => typeof(KeyValuePair<,>); 16 | 17 | private KeyValuePairFormatter() { } 18 | 19 | public override bool IsApplicable(object value) 20 | => value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition() == Type; 21 | 22 | public override StyledString FormatToText(object value, Level level, Formatter formatter) 23 | { 24 | var sb = new StyledStringBuilder(); 25 | 26 | dynamic kv = value; 27 | if (level == Level.FirstDetailed) 28 | { 29 | // KeyValuePair { key, value } 30 | sb.Append(formatter.FormatTypeName(value.GetType(), showNamespaces: false, useLanguageKeywords: true, hideSystemNamespace: true)); 31 | sb.Append(" { "); 32 | sb.Append(formatter.FormatObjectToText(kv.Key, level)); 33 | sb.Append(", "); 34 | sb.Append(formatter.FormatObjectToText(kv.Value, level)); 35 | } 36 | else if (level == Level.FirstSimple) 37 | { 38 | // { Key: key, Value: value } 39 | sb.Append("{ Key: "); 40 | sb.Append(formatter.FormatObjectToText(kv.Key, level)); 41 | sb.Append(", Value: "); 42 | sb.Append(formatter.FormatObjectToText(kv.Value, level)); 43 | } 44 | else 45 | { 46 | // { key, value } 47 | sb.Append("{ "); 48 | sb.Append(formatter.FormatObjectToText(kv.Key, level)); 49 | sb.Append(", "); 50 | sb.Append(formatter.FormatObjectToText(kv.Value, level)); 51 | } 52 | sb.Append(" }"); 53 | 54 | return sb.ToStyledString(); 55 | } 56 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/MethodInfoFormatter.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Linq; 6 | using System.Reflection; 7 | using CSharpRepl.Services.Theming; 8 | using Microsoft.CodeAnalysis.Classification; 9 | 10 | namespace CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 11 | 12 | internal sealed class MethodInfoFormatter : CustomObjectFormatter 13 | { 14 | public static readonly MethodInfoFormatter Instance = new(); 15 | 16 | private MethodInfoFormatter() { } 17 | 18 | public override StyledString FormatToText(MethodInfo value, Level level, Formatter formatter) 19 | { 20 | var methodNameStyle = formatter.GetStyle(ClassificationTypeNames.MethodName); 21 | var typeFormatter = TypeFormatter.Instance; 22 | 23 | var sb = new StyledStringBuilder(); 24 | 25 | //modifiers 26 | if (level is Level.FirstDetailed or Level.FirstSimple) 27 | { 28 | var modifiers = string.Join(" ", ReflectionHelpers.GetModifiers(value)); 29 | sb.Append(modifiers, formatter.KeywordStyle) 30 | .Append(' '); 31 | } 32 | 33 | //return type 34 | if (level < Level.ThirdPlus) 35 | { 36 | AppendReturnType(level is Level.FirstDetailed ? level : level.Increment()).Append(' '); 37 | } 38 | 39 | //name 40 | string name; 41 | if (level is Level.FirstDetailed) 42 | { 43 | name = value.Name; 44 | } 45 | else 46 | { 47 | var nameParts = value.Name.Split('.'); 48 | name = string.Join(".", nameParts.TakeLast(level is Level.FirstSimple ? 2 : 1)); //"interface.method" or "method" without namespace 49 | } 50 | sb.Append(name, methodNameStyle); 51 | 52 | if (level < Level.ThirdPlus) 53 | { 54 | //generic arguments 55 | if (value.IsGenericMethod) 56 | { 57 | sb.Append('<'); 58 | foreach (var a in value.GetGenericArguments()) 59 | { 60 | sb.Append(typeFormatter.FormatToText(a, level, formatter)); 61 | } 62 | sb.Append('>'); 63 | } 64 | 65 | //parameters 66 | sb.Append('('); 67 | var parameters = value.GetParameters(); 68 | for (int i = 0; i < parameters.Length; i++) 69 | { 70 | var p = parameters[i]; 71 | sb.Append(typeFormatter.FormatToText(p.ParameterType, level, formatter)); 72 | 73 | if (level < Level.Second) 74 | { 75 | sb.Append(' ').Append(p.Name); 76 | } 77 | 78 | if (i != parameters.Length - 1) sb.Append(", "); 79 | } 80 | sb.Append(')'); 81 | } 82 | 83 | return sb.ToStyledString(); 84 | 85 | StyledStringBuilder AppendReturnType(Level level) 86 | { 87 | return 88 | value.ReturnType == typeof(void) ? 89 | sb.Append(new StyledString("void", formatter.KeywordStyle)) : 90 | sb.Append(typeFormatter.FormatToText(value.ReturnType, level, formatter)); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/ReflectionHelpers.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | 8 | namespace CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 9 | 10 | internal static class ReflectionHelpers 11 | { 12 | public static IEnumerable GetModifiers(MethodInfo methodInfo) 13 | { 14 | if (methodInfo.IsPrivate) yield return "private"; 15 | else if (methodInfo.IsFamily) yield return "protected"; 16 | else if (methodInfo.IsFamilyOrAssembly) yield return "protected internal"; 17 | else if (methodInfo.IsFamilyAndAssembly) yield return "private protected"; 18 | else if (methodInfo.IsAssembly) yield return "internal"; 19 | else if (methodInfo.IsPublic) yield return "public"; 20 | 21 | if (methodInfo.IsStatic) yield return "static"; 22 | 23 | if (methodInfo.IsAbstract) yield return "abstract"; 24 | else if (methodInfo.IsVirtual) yield return "virtual"; 25 | } 26 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/TupleFormatter.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Runtime.CompilerServices; 6 | using CSharpRepl.Services.Theming; 7 | 8 | namespace CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 9 | 10 | internal sealed class TupleFormatter : CustomObjectFormatter 11 | { 12 | public static readonly TupleFormatter Instance = new(); 13 | 14 | private TupleFormatter() { } 15 | 16 | public override StyledString FormatToText(ITuple value, Level level, Formatter formatter) 17 | { 18 | var sb = new StyledStringBuilder(); 19 | 20 | sb.Append('('); 21 | for (int i = 0; i < value.Length; i++) 22 | { 23 | sb.Append(formatter.FormatObjectToText(value[i], level)); 24 | 25 | bool isLast = i == value.Length - 1; 26 | if (!isLast) sb.Append(", "); 27 | } 28 | sb.Append(')'); 29 | 30 | return sb.ToStyledString(); 31 | } 32 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/CustomObjectFormatters/TypeFormatter.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using CSharpRepl.Services.Theming; 7 | 8 | namespace CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 9 | 10 | internal sealed class TypeFormatter : CustomObjectFormatter 11 | { 12 | public static readonly TypeFormatter Instance = new(); 13 | 14 | private TypeFormatter() { } 15 | 16 | public override StyledString FormatToText(Type value, Level level, Formatter formatter) 17 | { 18 | return formatter.FormatTypeName( 19 | value, 20 | showNamespaces: level == Level.FirstDetailed, 21 | useLanguageKeywords: level != Level.FirstDetailed, 22 | hideSystemNamespace: false); 23 | } 24 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/FormattedObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Spectre.Console.Rendering; 3 | 4 | namespace CSharpRepl.Services.Roslyn.Formatting; 5 | 6 | internal readonly struct FormattedObject 7 | { 8 | public readonly IRenderable Renderable; 9 | public readonly object? Value; 10 | 11 | public FormattedObject(IRenderable renderable, object? value) 12 | { 13 | Renderable = renderable; 14 | Value = value; 15 | } 16 | 17 | public IEnumerable FormatMembers(PrettyPrinter prettyPrinter, Level level, bool includeNonPublic) 18 | { 19 | if (Value is null) return []; 20 | 21 | return prettyPrinter.FormatMembers(Value, level.Increment(), includeNonPublic); 22 | } 23 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/LengthLimiting.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using CSharpRepl.Services.Theming; 7 | using Spectre.Console; 8 | 9 | namespace CSharpRepl.Services.Roslyn.Formatting; 10 | 11 | public static class LengthLimiting 12 | { 13 | public static int GetMaxParagraphLength(Level level, Profile profile) 14 | { 15 | var ratio = level switch 16 | { 17 | Level.FirstDetailed => 2, 18 | Level.FirstSimple => 0.4, 19 | Level.Second => 0.2, 20 | Level.ThirdPlus => 0.1, 21 | _ => throw new InvalidOperationException("unexpected level") 22 | }; 23 | 24 | return (int)(ratio * profile.Width * profile.Height); 25 | } 26 | 27 | public static int GetTableMaxItems(Level level, Profile profile) 28 | { 29 | var ratio = level switch 30 | { 31 | Level.FirstDetailed => 2, 32 | Level.FirstSimple => 0.5, 33 | Level.Second => 0.4, 34 | Level.ThirdPlus => 0.3, 35 | _ => throw new InvalidOperationException("unexpected level") 36 | }; 37 | 38 | return (int)(ratio * profile.Height); 39 | } 40 | 41 | public static int GetTreeMaxItems(Level level, Profile profile) 42 | { 43 | var ratio = level switch 44 | { 45 | Level.FirstDetailed => 2, 46 | Level.FirstSimple => 0.5, 47 | Level.Second => 0.4, 48 | Level.ThirdPlus => 0.3, 49 | _ => throw new InvalidOperationException("unexpected level") 50 | }; 51 | 52 | return (int)(ratio * profile.Height); 53 | } 54 | 55 | public static string LimitLength(string? value, Level level, Profile profile) 56 | { 57 | if (string.IsNullOrEmpty(value)) return ""; 58 | var maxLen = GetMaxParagraphLength(level, profile); 59 | if (value.Length > maxLen) 60 | { 61 | value = string.Concat(value.AsSpan(0, maxLen), "..."); 62 | } 63 | return value; 64 | } 65 | 66 | public static StyledStringSegment LimitLength(StyledStringSegment value, Level level, Profile profile) 67 | { 68 | var maxLen = GetMaxParagraphLength(level, profile); 69 | if (value.Length > maxLen) 70 | { 71 | value = value.Substring(0, maxLen) + "..."; 72 | } 73 | return value; 74 | } 75 | 76 | public static StyledString LimitLength(StyledString value, Level level, Profile profile) 77 | { 78 | var maxLen = GetMaxParagraphLength(level, profile); 79 | if (value.Length > maxLen) 80 | { 81 | value = value.Substring(0, maxLen) + "..."; 82 | } 83 | return value; 84 | } 85 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/Level.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | 7 | namespace CSharpRepl.Services.Roslyn.Formatting; 8 | 9 | public enum Level 10 | { 11 | FirstDetailed, 12 | FirstSimple, 13 | Second, 14 | ThirdPlus 15 | } 16 | 17 | internal static class LevelX 18 | { 19 | public static Level Increment(this Level level) => (Level)Math.Min((int)level + 1, (int)Level.ThirdPlus); 20 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/Rendering/FormattedObjectRenderable.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Spectre.Console.Rendering; 7 | 8 | namespace CSharpRepl.Services.Roslyn.Formatting.Rendering; 9 | 10 | internal sealed class FormattedObjectRenderable : IRenderable 11 | { 12 | private readonly IRenderable renderable; 13 | private readonly bool renderOnNewLine; 14 | 15 | public FormattedObjectRenderable(IRenderable renderable, bool renderOnNewLine) 16 | { 17 | this.renderable = renderable; 18 | this.renderOnNewLine = renderOnNewLine; 19 | } 20 | 21 | public Measurement Measure(RenderOptions options, int maxWidth) 22 | => renderable.Measure(options, maxWidth); 23 | 24 | public IEnumerable Render(RenderOptions options, int maxWidth) 25 | { 26 | if (renderOnNewLine) 27 | { 28 | yield return Segment.LineBreak; 29 | } 30 | foreach (var segment in renderable.Render(options, maxWidth)) 31 | { 32 | yield return segment; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Formatting/Rendering/RenderableSequence.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Spectre.Console.Rendering; 7 | 8 | namespace CSharpRepl.Services.Roslyn.Formatting.Rendering; 9 | 10 | internal sealed class RenderableSequence : Renderable 11 | { 12 | private readonly Wrap[] items; 13 | 14 | public RenderableSequence(IRenderable r1, IRenderable r2, bool separateByLineBreak) 15 | { 16 | items = [new(r1, separateByLineBreak), new(r2, false)]; 17 | } 18 | 19 | protected override IEnumerable Render(RenderOptions options, int maxWidth) 20 | { 21 | foreach (var item in items) 22 | { 23 | foreach (var segment in item.Renderable.Render(options, maxWidth)) 24 | { 25 | yield return segment; 26 | } 27 | 28 | if (item.UseLineBreak) 29 | { 30 | yield return Segment.LineBreak; 31 | } 32 | } 33 | } 34 | 35 | private readonly struct Wrap 36 | { 37 | public readonly IRenderable Renderable; 38 | public readonly bool UseLineBreak; 39 | 40 | public Wrap(IRenderable renderable, bool useLineBreak) 41 | { 42 | Renderable = renderable; 43 | UseLineBreak = useLineBreak; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/MetadataResolvers/AlternativeReferenceResolver.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Immutable; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.CodeAnalysis; 9 | 10 | namespace CSharpRepl.Services.Roslyn.MetadataResolvers; 11 | /// 12 | /// An alternative to MetadataReferenceResolver.ResolveReference.
13 | /// This can be used when multiple references can be added from a single ResolveReference call, as Roslyn does not yet support it (https://github.com/dotnet/roslyn/issues/6900). 14 | ///
15 | public abstract class AlternativeReferenceResolver : IIndividualMetadataReferenceResolver 16 | { 17 | public static PortableExecutableReference DummyReference { get; } = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 18 | 19 | public ImmutableArray ResolveReference(string reference, string? baseFilePath, MetadataReferenceProperties properties, MetadataReferenceResolver compositeResolver) 20 | { 21 | if (CanResolve(reference)) 22 | return ImmutableArray.Create(DummyReference); 23 | 24 | return []; 25 | } 26 | 27 | public abstract bool CanResolve(string reference); 28 | 29 | public virtual Task> ResolveAsync(string reference, CancellationToken cancellationToken) 30 | => Task.FromResult(Resolve(reference)); 31 | 32 | public virtual ImmutableArray Resolve(string reference) 33 | => []; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/MetadataResolvers/CompositeAlternativeReferenceResolver.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.CodeAnalysis; 11 | 12 | namespace CSharpRepl.Services.Roslyn.MetadataResolvers; 13 | internal sealed class CompositeAlternativeReferenceResolver 14 | { 15 | private readonly AlternativeReferenceResolver[] alternativeResolvers; 16 | 17 | public CompositeAlternativeReferenceResolver(params AlternativeReferenceResolver[] alternativeResolvers) 18 | { 19 | this.alternativeResolvers = alternativeResolvers; 20 | } 21 | 22 | public async Task> GetAllAlternativeReferences(string code, CancellationToken cancellationToken) 23 | { 24 | var splitCommands = code.Split(new[] { '\r', '\n' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); 25 | 26 | var resolveReferencesTasks = 27 | from cmd in splitCommands 28 | from resolver in alternativeResolvers 29 | where resolver.CanResolve(cmd) 30 | select resolver.ResolveAsync(cmd, cancellationToken); 31 | 32 | return (await Task.WhenAll(resolveReferencesTasks)) 33 | .SelectMany(r => r) 34 | .ToImmutableArray(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/MetadataResolvers/CompositeMetadataReferenceResolver.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using System.Linq; 9 | using Microsoft.CodeAnalysis; 10 | 11 | namespace CSharpRepl.Services.Roslyn.MetadataResolvers; 12 | 13 | /// 14 | /// A that is contained by the . 15 | /// It gets a chance to resolve a reference; if it doesn't, the next is called. 16 | /// 17 | internal interface IIndividualMetadataReferenceResolver 18 | { 19 | ImmutableArray ResolveReference(string reference, string? baseFilePath, MetadataReferenceProperties properties, MetadataReferenceResolver compositeResolver); 20 | } 21 | 22 | /// 23 | /// A top-level metadata resolver. We can only specify a single in roslyn scripting. 24 | /// This composite class delegates to individual implementations (nuget resolver, assembly resolver, csproj resolver, etc). 25 | /// 26 | internal sealed class CompositeMetadataReferenceResolver : MetadataReferenceResolver, IEquatable 27 | { 28 | private readonly IIndividualMetadataReferenceResolver[] resolvers; 29 | 30 | public CompositeMetadataReferenceResolver(params IIndividualMetadataReferenceResolver[] resolvers) => 31 | this.resolvers = resolvers; 32 | 33 | public override ImmutableArray ResolveReference(string reference, string? baseFilePath, MetadataReferenceProperties properties) 34 | { 35 | reference = reference.Trim(); 36 | 37 | foreach (var resolver in resolvers) 38 | { 39 | var resolved = resolver.ResolveReference(reference, baseFilePath, properties, this); 40 | if (resolved.Any()) 41 | { 42 | return resolved; 43 | } 44 | } 45 | 46 | return []; 47 | } 48 | 49 | public override bool Equals(object? other) => 50 | Equals(other as CompositeMetadataReferenceResolver); 51 | 52 | public bool Equals(CompositeMetadataReferenceResolver? other) => 53 | other != null 54 | && EqualityComparer.Default.Equals(resolvers, other.resolvers); 55 | 56 | public override int GetHashCode() => 57 | HashCode.Combine(resolvers); 58 | } 59 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/MetadataResolvers/NugetPackageMetadataResolver.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Immutable; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using CSharpRepl.Services.Nuget; 10 | using Microsoft.CodeAnalysis; 11 | 12 | namespace CSharpRepl.Services.Roslyn.MetadataResolvers; 13 | 14 | /// 15 | /// Resolves nuget references, e.g. #r "nuget: Newtonsoft.Json" or #r "nuget: Newtonsoft.Json, 13.0.1" 16 | /// 17 | internal sealed class NugetPackageMetadataResolver : AlternativeReferenceResolver 18 | { 19 | private const string NugetPrefix = "nuget:"; 20 | private const string NugetPrefixWithHashR = "#r \"" + NugetPrefix; 21 | private readonly NugetPackageInstaller nugetInstaller; 22 | 23 | public NugetPackageMetadataResolver(IConsoleEx console, Configuration configuration) 24 | { 25 | this.nugetInstaller = new NugetPackageInstaller(console, configuration); 26 | } 27 | 28 | public override bool CanResolve(string reference) => 29 | reference.StartsWith(NugetPrefix, StringComparison.OrdinalIgnoreCase) || 30 | reference.StartsWith(NugetPrefixWithHashR, StringComparison.OrdinalIgnoreCase); // roslyn trims the "#r" prefix when passing to the resolver, but it has the prefix when called from our ScriptRunner 31 | 32 | public override Task> ResolveAsync(string reference, CancellationToken cancellationToken) 33 | { 34 | // we can be a bit loose in our parsing here, because we were more strict in IsNugetReference. 35 | // the 0th element will be the "nuget" keyword, which we ignore. 36 | var packageParts = reference.Split( 37 | new[] { "#r", "\"", ":", " ", ",", "/", "\\" }, 38 | StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries 39 | ); 40 | 41 | return packageParts.Length switch 42 | { 43 | 2 => nugetInstaller.InstallAsync(packageId: packageParts[1], cancellationToken: cancellationToken), 44 | 3 => nugetInstaller.InstallAsync(packageId: packageParts[1], version: packageParts[2].TrimStart('v'), cancellationToken), 45 | _ => throw new InvalidOperationException(@"Malformed nuget reference. Expected #r ""nuget: PackageName"" or #r ""nuget: PackageName, version""") 46 | }; 47 | } 48 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/MetadataResolvers/SolutionFileMetadataResolver.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Immutable; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using CSharpRepl.Services.Dotnet; 12 | using Microsoft.CodeAnalysis; 13 | using Microsoft.CodeAnalysis.MSBuild; 14 | 15 | namespace CSharpRepl.Services.Roslyn.MetadataResolvers; 16 | 17 | internal sealed class SolutionFileMetadataResolver : AlternativeReferenceResolver 18 | { 19 | private readonly DotnetBuilder builder; 20 | private readonly IConsoleEx console; 21 | 22 | public SolutionFileMetadataResolver(DotnetBuilder builder, IConsoleEx console) 23 | { 24 | this.builder = builder; 25 | this.console = console; 26 | } 27 | 28 | public override bool CanResolve(string reference) => 29 | reference.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) || 30 | reference.EndsWith(".sln\"", StringComparison.OrdinalIgnoreCase) || 31 | reference.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) || 32 | reference.EndsWith(".csproj\"", StringComparison.OrdinalIgnoreCase); 33 | 34 | public override async Task> ResolveAsync(string reference, CancellationToken cancellationToken) 35 | { 36 | var solutionPath = Path.GetFullPath(reference 37 | .Split('\"', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) 38 | .Last()); 39 | 40 | var (exitCode, _) = await builder.BuildAsync(solutionPath, cancellationToken); 41 | 42 | if (exitCode != 0) 43 | { 44 | console.WriteErrorLine("Reference not added: build failed."); 45 | return []; 46 | } 47 | 48 | console.WriteLine("Adding references from built project..."); 49 | var metadataReferences = await GetMetadataReferences(solutionPath, cancellationToken); 50 | return metadataReferences; 51 | } 52 | 53 | private async Task> GetMetadataReferences(string solutionOrProject, CancellationToken cancellationToken) 54 | { 55 | var workspace = MSBuildWorkspace.Create(); 56 | 57 | var projects = Path.GetExtension(solutionOrProject) switch 58 | { 59 | ".csproj" => [await workspace.OpenProjectAsync(solutionOrProject, cancellationToken: cancellationToken)], 60 | ".sln" => (await workspace.OpenSolutionAsync(solutionOrProject, cancellationToken: cancellationToken)).Projects, 61 | _ => throw new ArgumentException("Unexpected filetype for file " + solutionOrProject) 62 | }; 63 | 64 | foreach (var error in workspace.Diagnostics.Where(d => d.Kind == WorkspaceDiagnosticKind.Failure)) 65 | { 66 | console.WriteErrorLine(error.Message); 67 | } 68 | 69 | return projects 70 | .SelectMany(p => 71 | p.MetadataReferences 72 | .OfType() 73 | .Concat(p.OutputFilePath is not null 74 | ? [MetadataReference.CreateFromFile(p.OutputFilePath)] 75 | : Array.Empty() 76 | ) 77 | ) 78 | .Distinct() 79 | .ToImmutableArray(); 80 | } 81 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.CSharp.Symbols/GeneratedNameConstants.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.CodeAnalysis.CSharp.Symbols; 6 | 7 | internal static class GeneratedNameConstants 8 | { 9 | internal const char DotReplacementInTypeNames = '-'; 10 | internal const string SynthesizedLocalNamePrefix = "CS$"; 11 | internal const string SuffixSeparator = "__"; 12 | internal const char LocalFunctionNameTerminator = '|'; 13 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.CSharp.Symbols/GeneratedNameKind.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | 7 | namespace Microsoft.CodeAnalysis.CSharp.Symbols; 8 | 9 | internal enum GeneratedNameKind 10 | { 11 | None = 0, 12 | 13 | // Used by EE: 14 | ThisProxyField = '4', 15 | HoistedLocalField = '5', 16 | DisplayClassLocalOrField = '8', 17 | LambdaMethod = 'b', 18 | LambdaDisplayClass = 'c', 19 | StateMachineType = 'd', 20 | LocalFunction = 'g', // note collision with Deprecated_InitializerLocal, however this one is only used for method names 21 | 22 | // Used by EnC: 23 | AwaiterField = 'u', 24 | HoistedSynthesizedLocalField = 's', 25 | 26 | // Currently not parsed: 27 | StateMachineStateField = '1', 28 | IteratorCurrentBackingField = '2', 29 | StateMachineParameterProxyField = '3', 30 | ReusableHoistedLocalField = '7', 31 | LambdaCacheField = '9', 32 | FixedBufferField = 'e', 33 | FileType = 'F', 34 | AnonymousType = 'f', 35 | TransparentIdentifier = 'h', 36 | AnonymousTypeField = 'i', 37 | AnonymousTypeTypeParameter = 'j', 38 | AutoPropertyBackingField = 'k', 39 | IteratorCurrentThreadIdField = 'l', 40 | IteratorFinallyMethod = 'm', 41 | BaseMethodWrapper = 'n', 42 | AsyncBuilderField = 't', 43 | DelegateCacheContainerType = 'O', 44 | DynamicCallSiteContainerType = 'o', 45 | DynamicCallSiteField = 'p', 46 | AsyncIteratorPromiseOfValueOrEndBackingField = 'v', 47 | DisposeModeField = 'w', 48 | CombinedTokensField = 'x', // last 49 | 50 | // Deprecated - emitted by Dev12, but not by Roslyn. 51 | // Don't reuse the values because the debugger might encounter them when consuming old binaries. 52 | [Obsolete] 53 | Deprecated_OuterscopeLocals = '6', 54 | [Obsolete] 55 | Deprecated_IteratorInstance = 'a', 56 | [Obsolete] 57 | Deprecated_InitializerLocal = 'g', 58 | [Obsolete] 59 | Deprecated_DynamicDelegate = 'q', 60 | [Obsolete] 61 | Deprecated_ComrefCallLocal = 'r', 62 | } 63 | 64 | internal static class GeneratedNameKindExtensions 65 | { 66 | internal static bool IsTypeName(this GeneratedNameKind kind) 67 | => kind is GeneratedNameKind.LambdaDisplayClass 68 | or GeneratedNameKind.StateMachineType 69 | or GeneratedNameKind.DynamicCallSiteContainerType 70 | or GeneratedNameKind.DelegateCacheContainerType; 71 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.CSharp.Symbols/GeneratedNameParser.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | namespace Microsoft.CodeAnalysis.CSharp.Symbols; 9 | 10 | internal static class GeneratedNameParser 11 | { 12 | internal static bool IsSynthesizedLocalName(string name) 13 | => name.StartsWith(GeneratedNameConstants.SynthesizedLocalNamePrefix, StringComparison.Ordinal); 14 | 15 | // The type of generated name. See TryParseGeneratedName. 16 | internal static GeneratedNameKind GetKind(string name) 17 | => TryParseGeneratedName(name, out var kind, out _, out _) ? kind : GeneratedNameKind.None; 18 | 19 | // Parse the generated name. Returns true for names of the form 20 | // [CS$]<[middle]>c[__[suffix]] where [CS$] is included for certain 21 | // generated names, where [middle] and [__[suffix]] are optional, 22 | // and where c is a single character in [1-9a-z] 23 | // (csharp\LanguageAnalysis\LIB\SpecialName.cpp). 24 | internal static bool TryParseGeneratedName( 25 | string name, 26 | out GeneratedNameKind kind, 27 | out int openBracketOffset, 28 | out int closeBracketOffset) 29 | { 30 | openBracketOffset = -1; 31 | if (name.StartsWith("CS$<", StringComparison.Ordinal)) 32 | { 33 | openBracketOffset = 3; 34 | } 35 | else if (name.StartsWith("<", StringComparison.Ordinal)) 36 | { 37 | openBracketOffset = 0; 38 | } 39 | 40 | if (openBracketOffset >= 0) 41 | { 42 | closeBracketOffset = IndexOfBalancedParenthesis(name, openBracketOffset, '>'); 43 | if (closeBracketOffset >= 0 && closeBracketOffset + 1 < name.Length) 44 | { 45 | int c = name[closeBracketOffset + 1]; 46 | if (c is >= '1' and <= '9' or >= 'a' and <= 'z') // Note '0' is not special. 47 | { 48 | kind = (GeneratedNameKind)c; 49 | return true; 50 | } 51 | } 52 | } 53 | 54 | kind = GeneratedNameKind.None; 55 | openBracketOffset = -1; 56 | closeBracketOffset = -1; 57 | return false; 58 | } 59 | 60 | private static int IndexOfBalancedParenthesis(string str, int openingOffset, char closing) 61 | { 62 | char opening = str[openingOffset]; 63 | 64 | int depth = 1; 65 | for (int i = openingOffset + 1; i < str.Length; i++) 66 | { 67 | var c = str[i]; 68 | if (c == opening) 69 | { 70 | depth++; 71 | } 72 | else if (c == closing) 73 | { 74 | depth--; 75 | if (depth == 0) 76 | { 77 | return i; 78 | } 79 | } 80 | } 81 | 82 | return -1; 83 | } 84 | 85 | internal static bool TryParseSourceMethodNameFromGeneratedName(string generatedName, GeneratedNameKind requiredKind, [NotNullWhen(true)] out string? methodName) 86 | { 87 | if (!TryParseGeneratedName(generatedName, out var kind, out int openBracketOffset, out int closeBracketOffset)) 88 | { 89 | methodName = null; 90 | return false; 91 | } 92 | 93 | if (requiredKind != 0 && kind != requiredKind) 94 | { 95 | methodName = null; 96 | return false; 97 | } 98 | 99 | methodName = generatedName.Substring(openBracketOffset + 1, closeBracketOffset - openBracketOffset - 1); 100 | 101 | if (kind.IsTypeName()) 102 | { 103 | methodName = methodName.Replace(GeneratedNameConstants.DotReplacementInTypeNames, '.'); 104 | } 105 | 106 | return true; 107 | } 108 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.CSharp.Symbols/GeneratedNames.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.CodeAnalysis.CSharp.Symbols; 6 | 7 | internal static class GeneratedNames 8 | { 9 | internal static bool IsGeneratedMemberName(string memberName) 10 | { 11 | return memberName.Length > 0 && memberName[0] == '<'; 12 | } 13 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.PooledObjects/ArrayBuilder.Enumerator.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.CodeAnalysis.PooledObjects; 6 | 7 | internal partial class ArrayBuilder 8 | { 9 | /// 10 | /// struct enumerator used in foreach. 11 | /// 12 | internal struct Enumerator 13 | { 14 | private readonly ArrayBuilder _builder; 15 | private int _index; 16 | 17 | public Enumerator(ArrayBuilder builder) 18 | { 19 | _builder = builder; 20 | _index = -1; 21 | } 22 | 23 | public T Current 24 | { 25 | get 26 | { 27 | return _builder[_index]; 28 | } 29 | } 30 | 31 | public bool MoveNext() 32 | { 33 | _index++; 34 | return _index < _builder.Count; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.PooledObjects/PooledHashSet.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | 8 | namespace Microsoft.CodeAnalysis.PooledObjects; 9 | 10 | // HashSet that can be recycled via an object pool 11 | // NOTE: these HashSets always have the default comparer. 12 | internal sealed partial class PooledHashSet : HashSet 13 | { 14 | private readonly ObjectPool> _pool; 15 | 16 | private PooledHashSet(ObjectPool> pool, IEqualityComparer equalityComparer) : 17 | base(equalityComparer) 18 | { 19 | _pool = pool; 20 | } 21 | 22 | public void Free() 23 | { 24 | this.Clear(); 25 | _pool?.Free(this); 26 | } 27 | 28 | // global pool 29 | private static readonly ObjectPool> s_poolInstance = CreatePool(EqualityComparer.Default); 30 | 31 | // if someone needs to create a pool; 32 | public static ObjectPool> CreatePool(IEqualityComparer equalityComparer) 33 | { 34 | ObjectPool>? pool = null; 35 | pool = new ObjectPool>(() => new PooledHashSet(pool!, equalityComparer), 128); 36 | return pool; 37 | } 38 | 39 | public static PooledHashSet GetInstance() 40 | { 41 | var instance = s_poolInstance.Allocate(); 42 | Debug.Assert(instance.Count == 0); 43 | return instance; 44 | } 45 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.PooledObjects/PooledStringBuilder.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Text; 7 | 8 | namespace Microsoft.CodeAnalysis.PooledObjects; 9 | 10 | /// 11 | /// The usage is: 12 | /// var inst = PooledStringBuilder.GetInstance(); 13 | /// var sb = inst.builder; 14 | /// ... Do Stuff... 15 | /// ... sb.ToString() ... 16 | /// inst.Free(); 17 | /// 18 | internal sealed partial class PooledStringBuilder 19 | { 20 | public readonly StringBuilder Builder = new(); 21 | private readonly ObjectPool _pool; 22 | 23 | private PooledStringBuilder(ObjectPool pool) 24 | { 25 | Debug.Assert(pool != null); 26 | _pool = pool!; 27 | } 28 | 29 | public int Length 30 | { 31 | get { return this.Builder.Length; } 32 | } 33 | 34 | public void Free() 35 | { 36 | var builder = this.Builder; 37 | 38 | // do not store builders that are too large. 39 | if (builder.Capacity <= 1024) 40 | { 41 | builder.Clear(); 42 | _pool.Free(this); 43 | } 44 | else 45 | { 46 | _pool.ForgetTrackedObject(this); 47 | } 48 | } 49 | 50 | [System.Obsolete("Consider calling ToStringAndFree instead.")] 51 | public new string ToString() 52 | { 53 | return this.Builder.ToString(); 54 | } 55 | 56 | public string ToStringAndFree() 57 | { 58 | var result = this.Builder.ToString(); 59 | this.Free(); 60 | 61 | return result; 62 | } 63 | 64 | public string ToStringAndFree(int startIndex, int length) 65 | { 66 | var result = this.Builder.ToString(startIndex, length); 67 | this.Free(); 68 | 69 | return result; 70 | } 71 | 72 | // global pool 73 | private static readonly ObjectPool s_poolInstance = CreatePool(); 74 | 75 | // if someone needs to create a private pool; 76 | /// 77 | /// If someone need to create a private pool 78 | /// 79 | /// The size of the pool. 80 | /// 81 | public static ObjectPool CreatePool(int size = 32) 82 | { 83 | ObjectPool? pool = null; 84 | pool = new ObjectPool(() => new PooledStringBuilder(pool!), size); 85 | return pool; 86 | } 87 | 88 | public static PooledStringBuilder GetInstance() 89 | { 90 | var builder = s_poolInstance.Allocate(); 91 | Debug.Assert(builder.Builder.Length == 0); 92 | return builder; 93 | } 94 | 95 | public static implicit operator StringBuilder(PooledStringBuilder obj) 96 | { 97 | return obj.Builder; 98 | } 99 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.Scripting.Hosting/MemberFilter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Reflection; 6 | using Microsoft.CodeAnalysis.CSharp.Symbols; 7 | 8 | namespace Microsoft.CodeAnalysis.Scripting.Hosting; 9 | 10 | internal sealed class MemberFilter 11 | { 12 | public bool Include(MemberInfo member) 13 | => !IsGeneratedMemberName(member.Name); 14 | 15 | private bool IsGeneratedMemberName(string name) 16 | { 17 | // Generated fields, e.g. "k__BackingField" 18 | return GeneratedNames.IsGeneratedMemberName(name); 19 | } 20 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.Scripting.Hosting/PrimitiveFormatterOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Globalization; 6 | 7 | namespace Microsoft.CodeAnalysis.Scripting.Hosting; 8 | 9 | internal readonly struct PrimitiveFormatterOptions 10 | { 11 | /// 12 | /// Since is an extension point, we don't 13 | /// perform any validation on - it's up to the individual 14 | /// subtype. 15 | /// 16 | public int NumberRadix { get; } 17 | public bool IncludeCharacterCodePoints { get; } 18 | public bool QuoteStringsAndCharacters { get; } 19 | public bool EscapeNonPrintableCharacters { get; } 20 | public CultureInfo CultureInfo { get; } 21 | 22 | public PrimitiveFormatterOptions( 23 | int numberRadix, 24 | bool includeCodePoints, 25 | bool quoteStringsAndCharacters, 26 | bool escapeNonPrintableCharacters, 27 | CultureInfo cultureInfo) 28 | { 29 | NumberRadix = numberRadix; 30 | IncludeCharacterCodePoints = includeCodePoints; 31 | QuoteStringsAndCharacters = quoteStringsAndCharacters; 32 | EscapeNonPrintableCharacters = escapeNonPrintableCharacters; 33 | CultureInfo = cultureInfo; 34 | } 35 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis.Scripting.Hosting/TypeNameFormatterOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace Microsoft.CodeAnalysis.Scripting.Hosting; 6 | 7 | internal readonly struct TypeNameFormatterOptions 8 | { 9 | public readonly int ArrayBoundRadix; 10 | public readonly bool ShowNamespaces; 11 | public readonly bool UseLanguageKeywords; 12 | public readonly bool HideSystemNamespace; 13 | 14 | public TypeNameFormatterOptions(int arrayBoundRadix, bool showNamespaces, bool useLanguageKeywords, bool hideSystemNamespace) 15 | { 16 | ArrayBoundRadix = arrayBoundRadix; 17 | ShowNamespaces = showNamespaces; 18 | UseLanguageKeywords = useLanguageKeywords; 19 | HideSystemNamespace = hideSystemNamespace; 20 | } 21 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis/ObjectDisplayExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | #nullable disable 6 | 7 | namespace Microsoft.CodeAnalysis; 8 | 9 | internal static class ObjectDisplayExtensions 10 | { 11 | /// 12 | /// Determines if a flag is set on the enum. 13 | /// 14 | /// The value to check. 15 | /// An enum field that specifies the flag. 16 | /// Whether the is set on the . 17 | internal static bool IncludesOption(this ObjectDisplayOptions options, ObjectDisplayOptions flag) 18 | { 19 | return (options & flag) == flag; 20 | } 21 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Microsoft.CodeAnalysis/ObjectDisplayOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | #nullable disable 6 | 7 | using System; 8 | 9 | namespace Microsoft.CodeAnalysis; 10 | 11 | /// 12 | /// Specifies the options for how generics are displayed in the description of a symbol. 13 | /// 14 | [Flags] 15 | internal enum ObjectDisplayOptions 16 | { 17 | /// 18 | /// Format object using default options. 19 | /// 20 | None = 0, 21 | 22 | /// 23 | /// In C#, include the numeric code point before character literals. 24 | /// 25 | IncludeCodePoints = 1 << 0, 26 | 27 | /// 28 | /// Whether or not to include type suffix for applicable integral literals. 29 | /// 30 | IncludeTypeSuffix = 1 << 1, 31 | 32 | /// 33 | /// Whether or not to display integral literals in hexadecimal. 34 | /// 35 | UseHexadecimalNumbers = 1 << 2, 36 | 37 | /// 38 | /// Whether or not to quote character and string literals. 39 | /// 40 | UseQuotes = 1 << 3, 41 | 42 | /// 43 | /// In C#, replace non-printable (e.g. control) characters with dedicated (e.g. \t) or unicode (\u0001) escape sequences. 44 | /// In Visual Basic, replace non-printable characters with calls to ChrW and vb* constants. 45 | /// 46 | EscapeNonPrintableCharacters = 1 << 4, 47 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/References/AssemblyReferenceComparer.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Diagnostics.CodeAnalysis; 7 | using Microsoft.CodeAnalysis; 8 | 9 | namespace CSharpRepl.Services.Roslyn.References; 10 | 11 | /// 12 | /// Compares assembly references based on their filepath. 13 | /// 14 | internal sealed class AssemblyReferenceComparer : IEqualityComparer 15 | { 16 | public bool Equals(MetadataReference? x, MetadataReference? y) => 17 | x?.Display == y?.Display; 18 | 19 | public int GetHashCode([DisallowNull] MetadataReference obj) => 20 | (obj.Display ?? string.Empty).GetHashCode(); 21 | } 22 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/References/SharedFramework.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using Microsoft.CodeAnalysis; 10 | 11 | namespace CSharpRepl.Services.Roslyn.References; 12 | 13 | /// 14 | /// Represents an installed Shared Framework. Can be a base framework (Microsoft.NETCore.App), ASP.NET, Windows Desktop, etc. 15 | /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/metapackage-app?view=aspnetcore-5.0 16 | /// 17 | public class SharedFramework 18 | { 19 | public const string NetCoreApp = "Microsoft.NETCore.App"; 20 | public string ReferencePath { get; } 21 | public string ImplementationPath { get; } 22 | public IReadOnlyCollection ReferenceAssemblies { get; } 23 | public IReadOnlyCollection ImplementationAssemblies { get; } 24 | 25 | public SharedFramework( 26 | string referencePath, IReadOnlyCollection ReferenceAssemblies, 27 | string ImplementationPath, IReadOnlyCollection ImplementationAssemblies) 28 | { 29 | this.ReferencePath = referencePath; 30 | this.ImplementationPath = ImplementationPath; 31 | this.ReferenceAssemblies = ReferenceAssemblies; 32 | this.ImplementationAssemblies = ImplementationAssemblies; 33 | } 34 | 35 | public static string[] SupportedFrameworks { get; } = 36 | Path.GetDirectoryName(typeof(object).Assembly.Location) is string frameworkDirectory 37 | ? Directory 38 | .GetDirectories(Path.Combine(frameworkDirectory, "../../")) 39 | .Select(dir => Path.GetFileName(dir)) 40 | .ToArray() 41 | : []; 42 | 43 | public static Version ToDotNetVersion(string version) => 44 | // discard trailing preview versions, e.g. 6.0.0-preview.4.21253.7 45 | new Version(version.Split('-', 2).First()); 46 | } 47 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Scripting/EvaluationResult.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using Microsoft.CodeAnalysis; 8 | 9 | namespace CSharpRepl.Services.Roslyn.Scripting; 10 | 11 | /// about as close to a discriminated union as I can get 12 | public abstract record EvaluationResult 13 | { 14 | public sealed record Success(string Input, Optional ReturnValue, IReadOnlyCollection References) : EvaluationResult; 15 | public sealed record Error(Exception Exception) : EvaluationResult; 16 | public sealed record Cancelled() : EvaluationResult; 17 | } 18 | -------------------------------------------------------------------------------- /CSharpRepl.Services/Roslyn/Scripting/ScriptGlobals.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting; 7 | 8 | namespace CSharpRepl.Services.Roslyn.Scripting; 9 | 10 | /// 11 | /// Defines variables that are available in the C# Script environment. 12 | /// 13 | /// Must be public so it can be referenced by the script 14 | public sealed class ScriptGlobals 15 | { 16 | private readonly IConsoleEx console; 17 | 18 | public ScriptGlobals(IConsoleEx console, string[] args) 19 | { 20 | this.console = console; 21 | this.args = args; 22 | this.Args = new List(args); 23 | } 24 | 25 | #pragma warning disable IDE1006 // Naming Styles 26 | /// 27 | /// Arguments provided at the command line after a double dash. 28 | /// This naming convention matches top-level programs and Main method conventions. 29 | /// 30 | public string[] args { get; set; } 31 | #pragma warning restore IDE1006 // Naming Styles 32 | 33 | /// 34 | /// Arguments provided at the command line after a double dash. 35 | /// This naming convention matches csi, dotnet-script, etc. 36 | /// 37 | public IList Args { get; set; } 38 | 39 | /// 40 | /// Pretty-print a c# object. While not so useful in a REPL, where results 41 | /// are pretty-printed by default, it's useful in CSX scripts. 42 | /// 43 | public void Print(object value) => 44 | console.WriteLine(CSharpObjectFormatter.Instance.FormatObject(value)); 45 | } 46 | -------------------------------------------------------------------------------- /CSharpRepl.Services/RuntimeHelper.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | //This file loaded from resources and injected into CSharpRepl session in the warm-up phase. 6 | 7 | internal static class __CSharpRepl_RuntimeHelper 8 | { 9 | public static SpanOutput HandleSpanOutput(System.Span span) => SpanOutput.Create(span, typeof(System.Span)); 10 | public static SpanOutput HandleSpanOutput(System.ReadOnlySpan span) => SpanOutput.Create(span, typeof(System.ReadOnlySpan)); 11 | 12 | public static CharSpanOutput HandleSpanOutput(System.Span span) => CharSpanOutput.Create(span, typeof(System.Span)); 13 | public static CharSpanOutput HandleSpanOutput(System.ReadOnlySpan span) => CharSpanOutput.Create(span, typeof(System.ReadOnlySpan)); 14 | 15 | public static SpanOutput HandleMemoryOutput(System.Memory memory) => SpanOutput.Create(memory.Span, typeof(System.Memory)); 16 | public static SpanOutput HandleMemoryOutput(System.ReadOnlyMemory memory) => SpanOutput.Create(memory.Span, typeof(System.ReadOnlyMemory)); 17 | 18 | public static CharSpanOutput HandleMemoryOutput(System.Memory memory) => CharSpanOutput.Create(memory.Span, typeof(System.Memory)); 19 | public static CharSpanOutput HandleMemoryOutput(System.ReadOnlyMemory memory) => CharSpanOutput.Create(memory.Span, typeof(System.ReadOnlyMemory)); 20 | 21 | public static RefStructOutput HandleRefStructOutput(string text) => new(text); 22 | 23 | public abstract class SpanOutputBase(int originalLength, System.Type originalType) 24 | : System.Collections.IEnumerable 25 | { 26 | public readonly int OriginalLength = originalLength; 27 | public readonly System.Type OriginalType = originalType; 28 | 29 | //Necessary for correct output formatting. 30 | public int Count => OriginalLength; 31 | 32 | public abstract System.Collections.IEnumerator GetEnumerator(); 33 | } 34 | 35 | public sealed class SpanOutput(System.Array array, int originalLength, System.Type originalType) 36 | : SpanOutputBase(originalLength, originalType) 37 | { 38 | private const int MaxLength = 1024; 39 | 40 | private readonly System.Array array = array; 41 | 42 | public override System.Collections.IEnumerator GetEnumerator() => array.GetEnumerator(); 43 | 44 | public static SpanOutput Create(System.ReadOnlySpan span, System.Type originalType) => new( 45 | span[..System.Math.Min(MaxLength, span.Length)].ToArray(), 46 | span.Length, 47 | originalType); 48 | } 49 | 50 | public sealed class CharSpanOutput(string text, int originalLength, System.Type originalType) 51 | : SpanOutputBase(originalLength, originalType) 52 | { 53 | private const int MaxLength = 10_000; 54 | 55 | public readonly string Text = text; 56 | 57 | public override System.Collections.IEnumerator GetEnumerator() => Text.GetEnumerator(); 58 | 59 | public static CharSpanOutput Create(System.ReadOnlySpan span, System.Type originalType) 60 | { 61 | var len = System.Math.Min(MaxLength, span.Length); 62 | System.Span buffer = stackalloc char[len]; 63 | span[..len].CopyTo(buffer); 64 | if (span.Length > len) 65 | { 66 | buffer[^1] = '.'; 67 | buffer[^2] = '.'; 68 | buffer[^3] = '.'; 69 | } 70 | return new(buffer.ToString(), span.Length, originalType); 71 | } 72 | } 73 | 74 | public class RefStructOutput(string text) 75 | { 76 | private readonly string text = text; 77 | public override string ToString() => text; 78 | } 79 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/SymbolExploration/DebugSymbolLoader.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection.Metadata; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.SymbolStore; 13 | using Microsoft.SymbolStore.KeyGenerators; 14 | using Microsoft.SymbolStore.SymbolStores; 15 | 16 | namespace CSharpRepl.Services.SymbolExploration; 17 | 18 | /// 19 | /// Downloads PDB files from symbol servers for a given assembly file, 20 | /// and provides a for navigating them. 21 | /// 22 | /// 23 | /// This code has been adapted from https://github.com/dotnet/symstore/tree/main/src/dotnet-symbol 24 | /// 25 | internal sealed class DebugSymbolLoader : IDisposable 26 | { 27 | private readonly NullSymbolLogger logger; 28 | private readonly CacheSymbolStore symbolStore; 29 | private readonly SymbolStoreFile symbolStoreFile; 30 | 31 | private bool disposedValue; 32 | 33 | public DebugSymbolLoader(string assemblyFilePath) 34 | { 35 | this.logger = new NullSymbolLogger(); 36 | this.symbolStore = BuildSymbolStore(); 37 | this.symbolStoreFile = new SymbolStoreFile( 38 | File.Open(assemblyFilePath, FileMode.Open, FileAccess.Read, FileShare.Read), 39 | assemblyFilePath 40 | ); 41 | } 42 | 43 | /// 44 | /// Creates our configuration of symbol stores (either remote or a local cache). 45 | /// Symbol stores are chained, and if a given store does not contain the requested symbol 46 | /// the next one in the chain will be called. If a symbol store contains the requested symbol, 47 | /// previous symbols in the chain will have a chance to add them (this is how caching works). 48 | /// 49 | private CacheSymbolStore BuildSymbolStore() 50 | { 51 | SymbolStore? store = null; 52 | 53 | foreach (var server in Configuration.SymbolServers) 54 | { 55 | store = new HttpSymbolStore(logger, store, new Uri(server), accessToken: null); 56 | } 57 | 58 | var cacheDirectory = Path.Combine(Configuration.ApplicationDirectory, "symbols"); 59 | return new CacheSymbolStore(logger, store, cacheDirectory); 60 | } 61 | 62 | /// 63 | /// Calculate assembly key (i.e. an identifier) for the assembly. This key 64 | /// is used to query the symbol stores. 65 | /// 66 | public IEnumerable GetSymbolFileNames() => 67 | new FileKeyGenerator(logger, symbolStoreFile) 68 | .GetKeys(KeyTypeFlags.SymbolKey) 69 | .ToList(); 70 | 71 | public async Task DownloadSymbolFile(SymbolStoreKey key, CancellationToken cancellationToken) => 72 | await symbolStore.GetFile(key, cancellationToken); 73 | 74 | /// 75 | /// Produce a metadata reader we can use to navigate the PDB. 76 | /// 77 | public MetadataReader? ReadPortablePdb(SymbolStoreFile symbolFile) 78 | { 79 | if (symbolFile is null 80 | || !TryOpenPortablePdb(symbolFile, out var metadataReader) 81 | || metadataReader is null) 82 | { 83 | return null; 84 | } 85 | 86 | return metadataReader; 87 | } 88 | 89 | private static bool TryOpenPortablePdb(SymbolStoreFile symbolFile, out MetadataReader? mr) 90 | { 91 | try 92 | { 93 | var pdb = MetadataReaderProvider.FromPortablePdbStream(symbolFile.Stream, MetadataStreamOptions.Default); 94 | mr = pdb.GetMetadataReader(); 95 | return true; 96 | } 97 | catch (BadImageFormatException) // can happen if a Windows PDB is returned 98 | { 99 | mr = null; 100 | return false; 101 | } 102 | } 103 | 104 | public void Dispose() 105 | { 106 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 107 | Dispose(disposing: true); 108 | GC.SuppressFinalize(this); 109 | } 110 | 111 | private void Dispose(bool disposing) 112 | { 113 | if (!disposedValue) 114 | { 115 | if (disposing) 116 | { 117 | this.symbolStore.Dispose(); 118 | this.symbolStoreFile.Dispose(); 119 | } 120 | disposedValue = true; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /CSharpRepl.Services/SymbolExploration/NullSymbolLogger.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | namespace CSharpRepl.Services.SymbolExploration; 6 | 7 | internal sealed class NullSymbolLogger : Microsoft.SymbolStore.ITracer 8 | { 9 | public void WriteLine(string message) { } 10 | 11 | public void WriteLine(string format, params object[] arguments) { } 12 | 13 | public void Information(string message) { } 14 | 15 | public void Information(string format, params object[] arguments) { } 16 | 17 | public void Warning(string message) { } 18 | 19 | public void Warning(string format, params object[] arguments) { } 20 | 21 | public void Error(string message) { } 22 | 23 | public void Error(string format, params object[] arguments) { } 24 | 25 | public void Verbose(string message) { } 26 | 27 | public void Verbose(string format, params object[] arguments) { } 28 | } 29 | -------------------------------------------------------------------------------- /CSharpRepl.Services/SymbolExploration/SourceLink/GitHubSourceLinkHost.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Linq; 6 | using System.Reflection.Metadata; 7 | 8 | namespace CSharpRepl.Services.SymbolExploration; 9 | 10 | /// 11 | /// Rewrites the plaintext github URLs to the web UI URLs. 12 | /// i.e. raw.githubusercontent.com -> www.github.com 13 | /// 14 | internal sealed class GitHubSourceLinkHost 15 | { 16 | public string? Rewrite(string url, SequencePointRange sequencePointRange) 17 | { 18 | var parts = url.Split('/').ToList(); 19 | if (parts.Count <= 5 || parts[2] != "raw.githubusercontent.com") 20 | { 21 | return null; 22 | } 23 | 24 | // generate a URL to the source code based on Sequence Point info 25 | 26 | parts[2] = "www.github.com"; 27 | parts.Insert(5, "blob"); 28 | return string.Join("/", parts) + CreateLineNumberLocationHash(sequencePointRange); 29 | } 30 | 31 | private string CreateLineNumberLocationHash(SequencePointRange sequencePointRange) 32 | { 33 | SequencePoint lineStart = sequencePointRange.Start; 34 | SequencePoint? lineEnd = sequencePointRange.End; 35 | 36 | // design choice: if we can't highlight a range, don't highlight 37 | // anything. Otherwise, it's confusing for the user if we 38 | // highlight a random line in the source file. 39 | if (lineEnd is null) return string.Empty; 40 | 41 | return "#L" + lineStart.StartLine 42 | + (lineEnd is not null ? "-L" + lineEnd.Value.EndLine : ""); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CSharpRepl.Services/SymbolExploration/SourceLink/SourceLinkLookup.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Reflection.Metadata; 8 | using System.Text.Json; 9 | using System.Text.Json.Serialization; 10 | using System.Text.RegularExpressions; 11 | 12 | namespace CSharpRepl.Services.SymbolExploration; 13 | 14 | /// 15 | /// Looks up SourceLink JSON metadata in a PDB for a given Sequence Point to get 16 | /// the URL of the document, e.g. on GitHub. 17 | /// 18 | /// 19 | /// The following archived code was used as reference for how to interact with sourcelink json documents: 20 | /// https://github.com/ctaggart/SourceLink/blob/master/dotnet-sourcelink/Program.cs 21 | /// See also 22 | /// https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#sequence-points-blob 23 | /// 24 | internal sealed class SourceLinkLookup 25 | { 26 | private static readonly Guid SourceLinkId = new("CC110556-A091-4D38-9FEC-25AB9A351A6A"); 27 | private readonly GitHubSourceLinkHost[] sourceLinkHosts; 28 | 29 | public SourceLinkLookup() 30 | { 31 | // if a host is not in this list, we pass back the link unmodified. 32 | this.sourceLinkHosts = [new GitHubSourceLinkHost()]; 33 | } 34 | 35 | public bool TryGetSourceLinkUrl(MetadataReader symbolReader, SequencePointRange sequencePointRange, out string? url) 36 | { 37 | var sourceLinkMetadata = FindSourceLinkMetadata(symbolReader); 38 | 39 | var sequencePointDocumentName = symbolReader.GetString( 40 | symbolReader.GetDocument(sequencePointRange.Start.Document).Name 41 | ); 42 | 43 | url = GetUrl(sourceLinkMetadata, sequencePointDocumentName); 44 | 45 | if (url is null) return false; 46 | 47 | // optionally rewrite the url to something more friendly, based on the host. 48 | foreach (var host in sourceLinkHosts) 49 | { 50 | if (host.Rewrite(url, sequencePointRange) is string rewrittenUrl) 51 | { 52 | url = rewrittenUrl; 53 | break; 54 | } 55 | } 56 | return true; 57 | } 58 | 59 | /// 60 | /// Returns the source link json for the provided PDB metadata reader 61 | /// 62 | private static SourceLinkJson? FindSourceLinkMetadata(MetadataReader symbolReader) 63 | { 64 | // https://github.com/ctaggart/SourceLink/blob/master/dotnet-sourcelink/Program.cs 65 | var blobh = default(BlobHandle); 66 | foreach (var cdih in symbolReader.GetCustomDebugInformation(EntityHandle.ModuleDefinition)) 67 | { 68 | var cdi = symbolReader.GetCustomDebugInformation(cdih); 69 | if (symbolReader.GetGuid(cdi.Kind) == SourceLinkId) 70 | blobh = cdi.Value; 71 | } 72 | 73 | var utf8JsonBytes = symbolReader.GetBlobBytes(blobh); 74 | var json = JsonSerializer.Deserialize(utf8JsonBytes); 75 | return json; 76 | } 77 | 78 | /// 79 | /// Retrieves the SourceLink URL for the provided Sequence Points Blob document. 80 | /// 81 | private static string? GetUrl(SourceLinkJson? json, string? file) 82 | { 83 | if (json is null || file is null) return null; 84 | 85 | foreach (var key in json.Documents.Keys) 86 | { 87 | if (key.Contains('*')) 88 | { 89 | var pattern = Regex.Escape(key).Replace(@"\*", "(.+)"); 90 | var regex = new Regex(pattern); 91 | var m = regex.Match(file); 92 | if (!m.Success) continue; 93 | var url = json.Documents[key]; 94 | var path = m.Groups[1].Value.Replace(@"\", "/"); 95 | return url.Replace("*", path); 96 | } 97 | else 98 | { 99 | if (!key.Equals(file, StringComparison.Ordinal)) continue; 100 | return json.Documents[key]; 101 | } 102 | } 103 | return null; 104 | } 105 | 106 | private record SourceLinkJson( 107 | [property: JsonPropertyName("documents")] IDictionary Documents 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /CSharpRepl.Services/SyntaxHighlighting/HighlightedSpan.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Microsoft.CodeAnalysis.Text; 8 | using PrettyPrompt.Highlighting; 9 | 10 | namespace CSharpRepl.Services.SyntaxHighlighting; 11 | 12 | public sealed record HighlightedSpan(TextSpan TextSpan, AnsiColor Color); 13 | 14 | public static class HighlightedSpanExtensions 15 | { 16 | public static IReadOnlyCollection ToFormatSpans(this IReadOnlyCollection spans) => spans 17 | .Select(span => new FormatSpan( 18 | span.TextSpan.Start, 19 | span.TextSpan.Length, 20 | new ConsoleFormat(Foreground: span.Color) 21 | )) 22 | .ToArray(); 23 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/SyntaxHighlighting/SyntaxHighlighter.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using CSharpRepl.Services.Theming; 11 | using Microsoft.CodeAnalysis; 12 | using Microsoft.CodeAnalysis.Classification; 13 | using Microsoft.CodeAnalysis.Text; 14 | using Microsoft.Extensions.Caching.Memory; 15 | using PrettyPrompt.Highlighting; 16 | using Spectre.Console; 17 | 18 | namespace CSharpRepl.Services.SyntaxHighlighting; 19 | 20 | /// 21 | /// Invokes roslyn's classification API on a code document, and combines the 22 | /// classifications with a theme file to determine the resulting spans of color. 23 | /// 24 | internal sealed class SyntaxHighlighter 25 | { 26 | private const string CacheKeyPrefix = "SyntaxHighlighter_"; 27 | private readonly Theme theme; 28 | private readonly AnsiColor unhighlightedAnsiColor; 29 | private readonly Color unhighlightedSpectreColor; 30 | private readonly MemoryCache cache; 31 | 32 | public SyntaxHighlighter(MemoryCache cache, Theme theme) 33 | { 34 | this.cache = cache; 35 | this.theme = theme; 36 | this.unhighlightedAnsiColor = theme.GetSyntaxHighlightingAnsiColor("text", AnsiColor.White); 37 | this.unhighlightedSpectreColor = theme.GetSyntaxHighlightingSpectreColor("text", Color.White); 38 | } 39 | 40 | internal async Task> HighlightAsync(Document document) 41 | { 42 | var text = (await document.GetTextAsync()).ToString(); 43 | var cacheKey = CacheKeyPrefix + document.Name + text; 44 | if (this.cache.Get>(cacheKey) is IReadOnlyCollection spans) 45 | return spans; 46 | 47 | var classified = await Classifier.GetClassifiedSpansAsync(document, TextSpan.FromBounds(0, text.Length)).ConfigureAwait(false); 48 | 49 | // we can have multiple classifications for a given span. Choose the first one that has a corresponding color in the theme. 50 | var highlighted = classified 51 | .GroupBy(classification => classification.TextSpan) 52 | .Select(classifications => 53 | { 54 | var highlight = classifications 55 | .Select(classification => theme.GetSyntaxHighlightingAnsiColor(classification.ClassificationType)) 56 | .FirstOrDefault(themeColor => themeColor is not null) 57 | ?? unhighlightedAnsiColor; 58 | return new HighlightedSpan(classifications.Key, highlight); 59 | }) 60 | .ToList(); 61 | 62 | this.cache.Set(cacheKey, highlighted, DateTimeOffset.Now.AddMinutes(1)); 63 | 64 | return highlighted; 65 | } 66 | 67 | public AnsiColor GetAnsiColor(string? classification) => theme.GetSyntaxHighlightingAnsiColor(classification, unhighlightedAnsiColor); 68 | public bool TryGetAnsiColor(string? classification, out AnsiColor color) => theme.TryGetSyntaxHighlightingAnsiColor(classification, out color); 69 | public ConsoleFormat GetFormat(string? classification) => new(Foreground: GetAnsiColor(classification)); 70 | public bool TryGetFormat(string? classification, out ConsoleFormat format) 71 | { 72 | if (theme.TryGetSyntaxHighlightingAnsiColor(classification, out var color)) 73 | { 74 | format = new ConsoleFormat(Foreground: color); 75 | return true; 76 | } 77 | else 78 | { 79 | format = ConsoleFormat.None; 80 | return false; 81 | } 82 | } 83 | 84 | public Color GetSpectreColor(string? classification) => theme.GetSyntaxHighlightingSpectreColor(classification, unhighlightedSpectreColor); 85 | public bool TryGetSpectreColor(string? classification, out Color color) => theme.TryGetSyntaxHighlightingSpectreColor(classification, out color); 86 | public Style GetStyle(string? classification) => new(foreground: GetSpectreColor(classification)); 87 | public bool TryGetStyle(string? classification, [NotNullWhen(true)] out Style? style) 88 | { 89 | if (theme.TryGetSyntaxHighlightingSpectreColor(classification, out var color)) 90 | { 91 | style = new Style(foreground: color); 92 | return true; 93 | } 94 | else 95 | { 96 | style = null; 97 | return false; 98 | } 99 | } 100 | 101 | public Style KeywordStyle => GetStyle(ClassificationTypeNames.Keyword); 102 | public ConsoleFormat KeywordFormat => GetFormat(ClassificationTypeNames.Keyword); 103 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/SystemConsoleEx.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using PrettyPrompt.Consoles; 7 | using Spectre.Console; 8 | using Spectre.Console.Rendering; 9 | 10 | namespace CSharpRepl.Services; 11 | 12 | public sealed class SystemConsoleEx : IConsoleEx 13 | { 14 | private readonly IAnsiConsole ansiConsole = AnsiConsole.Console; 15 | 16 | public IConsole PrettyPromptConsole { get; } = new SystemConsole(); 17 | 18 | public Profile Profile => ansiConsole.Profile; 19 | public IAnsiConsoleCursor Cursor => ansiConsole.Cursor; 20 | public IAnsiConsoleInput Input => ansiConsole.Input; 21 | public IExclusivityMode ExclusivityMode => ansiConsole.ExclusivityMode; 22 | public RenderPipeline Pipeline => ansiConsole.Pipeline; 23 | public void Clear(bool home) => ansiConsole.Clear(home); 24 | 25 | public void Write(IRenderable renderable) => ansiConsole.Write(renderable); 26 | 27 | public string? ReadLine() => Console.ReadLine(); 28 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Theming/ExportVisualStudioTheme.csx: -------------------------------------------------------------------------------- 1 | //Expecting path to .vstheme file in first argument. 2 | //.vstheme is xml file that can be generated by https://marketplace.visualstudio.com/items?itemName=idex.colorthemedesigner2022 3 | //This script will extract C# highlighting informations and will generate appropriate json file alongside original vstheme file. 4 | 5 | #r "nuget: Microsoft.CodeAnalysis.Workspaces.Common" 6 | 7 | using System; 8 | using System.IO; 9 | using System.Xml.Linq; 10 | using System.Text.Json; 11 | using System.Reflection; 12 | using Microsoft.CodeAnalysis.Classification; 13 | 14 | const string UnknownValue = "???"; 15 | 16 | var vsTheme = XElement.Load(args[0]); 17 | string GetVsColor(string name) 18 | { 19 | XElement group; 20 | if (name.Equals("text", StringComparison.OrdinalIgnoreCase) || 21 | name.Equals("identifier", StringComparison.OrdinalIgnoreCase)) 22 | { 23 | GetPlainTextInfo(out group, out name); 24 | } 25 | else 26 | { 27 | group = vsTheme; 28 | } 29 | 30 | var colorElem = GetColorElement(group, name, out var hasValue); 31 | if (!hasValue) 32 | { 33 | if (name.Equals("record class name", StringComparison.OrdinalIgnoreCase)) 34 | { 35 | name = "class name"; 36 | } 37 | else if (name.Equals("record struct name", StringComparison.OrdinalIgnoreCase)) 38 | { 39 | name = "struct name"; 40 | } 41 | else if ( 42 | name.Equals("field name", StringComparison.OrdinalIgnoreCase) || 43 | name.Equals("enum member name", StringComparison.OrdinalIgnoreCase) || 44 | name.Equals("constant name", StringComparison.OrdinalIgnoreCase) || 45 | name.Equals("property name", StringComparison.OrdinalIgnoreCase) || 46 | name.Equals("event name", StringComparison.OrdinalIgnoreCase) || 47 | name.Equals("namespace name", StringComparison.OrdinalIgnoreCase) || 48 | name.Equals("label name", StringComparison.OrdinalIgnoreCase)) 49 | { 50 | //no special coloring in VS 51 | GetPlainTextInfo(out group, out name); 52 | } 53 | 54 | colorElem = GetColorElement(group, name, out hasValue); 55 | if (!hasValue) return UnknownValue; 56 | } 57 | 58 | var color = colorElem.Attribute("Source")?.Value ?? UnknownValue; 59 | if (color.Length == 8) color = color.Substring(2); //throw alpha channel away (eg. from FF8BE9FD) 60 | return "#" + color; 61 | 62 | static XElement GetColorElement(XElement group, string name, out bool hasValue) 63 | { 64 | var colorElem = group 65 | .Descendants("Color") 66 | .SingleOrDefault(e => e.Attribute("Name")?.Value.Equals(name, StringComparison.OrdinalIgnoreCase) == true) 67 | ?.Element("Foreground"); 68 | hasValue = colorElem != null && colorElem.Attribute("Type")?.Value == "CT_RAW"; 69 | return colorElem; 70 | } 71 | 72 | void GetPlainTextInfo(out XElement group, out string name) 73 | { 74 | group = vsTheme.Descendants().Where(e => e.Attribute("Name")?.Value == "Text Editor Text Manager Items").Single(); 75 | name = "plain text"; 76 | } 77 | } 78 | 79 | record Color(string name, string foreground); 80 | record Colors(Color[] syntaxHighlightingColors); 81 | 82 | var colors = 83 | typeof(ClassificationTypeNames) 84 | .GetFields(BindingFlags.Public | BindingFlags.Static) 85 | .Where(f => f.FieldType == typeof(string)) 86 | .Select(f => (string)f.GetValue(null)) 87 | .Where(name => 88 | !name.Equals("whitespace", StringComparison.OrdinalIgnoreCase) && 89 | !name.Equals("static symbol", StringComparison.OrdinalIgnoreCase)) //??? 90 | .Select( 91 | name => 92 | { 93 | Console.WriteLine(name); 94 | return new Color(name, GetVsColor(name)); 95 | }) 96 | .ToArray(); 97 | 98 | var jsonString = JsonSerializer.Serialize(new Colors(colors), new JsonSerializerOptions() { WriteIndented = true }); 99 | File.WriteAllText(Path.ChangeExtension(args[0], "json"), jsonString); -------------------------------------------------------------------------------- /CSharpRepl.Services/Theming/StyledString.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using CSharpRepl.Services.Extensions; 9 | using PrettyPrompt.Documents; 10 | using Spectre.Console; 11 | 12 | namespace CSharpRepl.Services.Theming; 13 | 14 | public readonly struct StyledString 15 | { 16 | public static readonly StyledString Empty = new(""); 17 | 18 | private readonly List parts; 19 | 20 | public int Length { get; } 21 | public bool IsEmpty => parts.Count == 0; 22 | public IReadOnlyList Parts => parts; 23 | 24 | public StyledString(StyledStringSegment part) 25 | { 26 | if (part.Length > 0) 27 | { 28 | parts = [part]; 29 | Length = part.Length; 30 | } 31 | else 32 | { 33 | parts = Empty.parts ?? []; 34 | } 35 | } 36 | 37 | public StyledString(string part, Style? style = null) 38 | : this(new StyledStringSegment(part, style)) 39 | { } 40 | 41 | public StyledString(IEnumerable parts) 42 | { 43 | this.parts = []; 44 | int len = 0; 45 | foreach (var part in parts) 46 | { 47 | if (part.Length > 0) 48 | { 49 | this.parts.Add(part); 50 | len += part.Length; 51 | } 52 | } 53 | Length = len; 54 | } 55 | 56 | public char FirstChar => parts[0].Text[0]; 57 | public char LastChar => parts[^1].Text[0]; 58 | 59 | /// 60 | /// 61 | /// 62 | public StyledString Substring(int startIndex, int length) 63 | { 64 | //formal argument validation will be done in Text.Substring(...) 65 | Debug.Assert(startIndex >= 0 && startIndex <= Length); 66 | Debug.Assert(length >= 0 && length - startIndex <= Length); 67 | 68 | if (IsEmpty || length == 0) return Empty; 69 | if (length - startIndex == Length) return this; 70 | 71 | var resultParts = new List(parts.Count); 72 | int i = 0; 73 | foreach (var part in parts) 74 | { 75 | var partSpan = new TextSpan(i, part.Length); 76 | if (partSpan.Overlap(startIndex, length).TryGet(out var newSpan)) 77 | { 78 | resultParts.Add(new StyledStringSegment(part.Text.Substring(newSpan.Start - i, newSpan.Length), part.Style)); 79 | } 80 | i += part.Length; 81 | } 82 | 83 | return new StyledString(resultParts); 84 | } 85 | 86 | public Paragraph ToParagraph() 87 | { 88 | var p = new Paragraph(); 89 | foreach (var part in parts) 90 | { 91 | p.Append(part.Text, part.Style); 92 | } 93 | return p; 94 | } 95 | 96 | public override string ToString() => string.Join("", parts); 97 | 98 | public static implicit operator StyledString(string text) => new(new StyledStringSegment(text)); 99 | public static implicit operator StyledString(StyledStringSegment text) => new(text); 100 | 101 | public static StyledString operator +(StyledString a, StyledString b) => new(a.parts.Concat(b.parts)); 102 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Theming/StyledStringBuilder.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using Spectre.Console; 8 | 9 | namespace CSharpRepl.Services.Theming; 10 | 11 | public sealed class StyledStringBuilder 12 | { 13 | private readonly List parts = []; 14 | 15 | private int length; 16 | public int Length => length; 17 | 18 | public StyledStringBuilder() 19 | { } 20 | 21 | public StyledStringBuilder(StyledStringSegment text) 22 | { 23 | Add(text); 24 | } 25 | 26 | public StyledStringBuilder(string? text, Style? style) 27 | { 28 | if (text != null) 29 | { 30 | Add(new StyledStringSegment(text, style)); 31 | } 32 | } 33 | 34 | //string has implicit conversion to Style which is not desirable here 35 | [Obsolete("Do not use this overload. You need to pass Style object and not string implicitly parsed to Style.")] 36 | public StyledStringBuilder(string? text, string style) 37 | => throw new NotSupportedException(); 38 | 39 | public StyledStringBuilder Append(string? text, Style? style = null) 40 | { 41 | if (text != null) 42 | { 43 | Add(new StyledStringSegment(text, style)); 44 | } 45 | return this; 46 | } 47 | 48 | //string has implicit conversion to Style which is not desirable here 49 | [Obsolete("Do not use this overload. You need to pass Style object and not string implicitly parsed to Style.")] 50 | public StyledStringBuilder Append(string? text, string style) 51 | => throw new NotSupportedException(); 52 | 53 | public StyledStringBuilder Append(char c, Style? style = null) 54 | { 55 | Add(new StyledStringSegment(c.ToString(), style)); 56 | return this; 57 | } 58 | 59 | public StyledStringBuilder Append(StyledString text) 60 | { 61 | foreach (var part in text.Parts) 62 | { 63 | Add(part); 64 | } 65 | return this; 66 | } 67 | 68 | public StyledStringBuilder Append(StyledStringSegment text) 69 | { 70 | Add(text); 71 | return this; 72 | } 73 | 74 | private void Add(StyledStringSegment text) 75 | { 76 | parts.Add(text); 77 | length += text.Length; 78 | } 79 | 80 | public override string ToString() => string.Join("", parts); 81 | public StyledString ToStyledString() => new(parts); 82 | 83 | public void Clear() 84 | { 85 | parts.Clear(); 86 | length = 0; 87 | } 88 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Theming/StyledStringSegment.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using Spectre.Console; 6 | 7 | namespace CSharpRepl.Services.Theming; 8 | 9 | public readonly struct StyledStringSegment 10 | { 11 | public static StyledStringSegment Empty => new(""); 12 | 13 | public readonly string Text; 14 | public readonly Style? Style; 15 | 16 | public int Length => Text.Length; 17 | 18 | public StyledStringSegment(string text, Style? style = null) 19 | { 20 | Text = text; 21 | Style = style; 22 | } 23 | 24 | public override string ToString() => Text; 25 | 26 | public static implicit operator StyledStringSegment(string text) => new(text, null); 27 | 28 | public Paragraph ToParagraph() => new(Text, Style); 29 | 30 | /// 31 | /// 32 | /// 33 | public StyledStringSegment Substring(int startIndex, int length) 34 | => new(Text.Substring(startIndex, length), Style); 35 | } -------------------------------------------------------------------------------- /CSharpRepl.Services/Theming/SyntaxHighlightingColor.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Diagnostics; 6 | using System.Text.Json.Serialization; 7 | 8 | namespace CSharpRepl.Services.Theming; 9 | 10 | public readonly struct SyntaxHighlightingColor 11 | { 12 | [JsonConstructor] 13 | public SyntaxHighlightingColor(string name, string foreground) 14 | { 15 | Debug.Assert(!string.IsNullOrEmpty(name)); 16 | 17 | Name = name; 18 | Foreground = foreground; 19 | } 20 | 21 | public string Name { get; } 22 | public string Foreground { get; } 23 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/CSharpRepl.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | all 26 | 27 | 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | all 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/ConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using CSharpRepl.Services; 7 | using Xunit; 8 | 9 | namespace CSharpRepl.Tests; 10 | 11 | public class ConfigurationTests 12 | { 13 | [Theory] 14 | [InlineData("Enter", ConsoleKey.Enter, '\n', default(ConsoleModifiers))] 15 | [InlineData("Control+Enter", ConsoleKey.Enter, '\n', ConsoleModifiers.Control)] 16 | [InlineData("Control+Alt+Enter", ConsoleKey.Enter, '\n', ConsoleModifiers.Control | ConsoleModifiers.Alt)] 17 | [InlineData("ctrl+enter", ConsoleKey.Enter, '\n', ConsoleModifiers.Control)] 18 | [InlineData("A", ConsoleKey.A, 'A', default(ConsoleModifiers))] 19 | [InlineData("a", ConsoleKey.A, 'A', default(ConsoleModifiers))] 20 | [InlineData("Alt+Shift+A", ConsoleKey.A, 'A', ConsoleModifiers.Alt | ConsoleModifiers.Shift)] 21 | public void ParseKeyPressPattern_Key(string pattern, ConsoleKey expectedKey, char expectedChar, ConsoleModifiers expectedModifiers) 22 | { 23 | var parsed = Configuration.ParseKeyPressPattern(pattern); 24 | Assert.Equal(expectedKey, parsed.Key); 25 | Assert.Equal(expectedChar, parsed.Character); 26 | Assert.Equal(expectedModifiers, parsed.Modifiers); 27 | } 28 | 29 | [Theory] 30 | [InlineData("(", '(')] 31 | [InlineData(".", '.')] 32 | [InlineData(" ", ' ')] 33 | public void ParseKeyPressPattern_KeyChar(string pattern, char keyChar) 34 | { 35 | var parsed = Configuration.ParseKeyPressPattern(pattern); 36 | Assert.Equal(default, parsed.Key); 37 | Assert.Equal(keyChar, parsed.Character); 38 | Assert.Equal(default, parsed.Modifiers); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ComplexSolution/ComplexSolution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntryPoint", "EntryPoint\EntryPoint.csproj", "{EBFD51FA-9FEA-4DA4-A846-6ED5ED194B2F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibraryA", "LibraryA\LibraryA.csproj", "{4FCD8E52-E2EA-48D8-A32C-37528E60274D}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibraryB", "LibraryB\LibraryB.csproj", "{A2465C3B-352F-4192-8BCA-85E94E86AEB9}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {EBFD51FA-9FEA-4DA4-A846-6ED5ED194B2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {EBFD51FA-9FEA-4DA4-A846-6ED5ED194B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {EBFD51FA-9FEA-4DA4-A846-6ED5ED194B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {EBFD51FA-9FEA-4DA4-A846-6ED5ED194B2F}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {4FCD8E52-E2EA-48D8-A32C-37528E60274D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {4FCD8E52-E2EA-48D8-A32C-37528E60274D}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {4FCD8E52-E2EA-48D8-A32C-37528E60274D}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {4FCD8E52-E2EA-48D8-A32C-37528E60274D}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {A2465C3B-352F-4192-8BCA-85E94E86AEB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {A2465C3B-352F-4192-8BCA-85E94E86AEB9}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {A2465C3B-352F-4192-8BCA-85E94E86AEB9}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {A2465C3B-352F-4192-8BCA-85E94E86AEB9}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ComplexSolution/EntryPoint/EntryPoint.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ComplexSolution/EntryPoint/Program.cs: -------------------------------------------------------------------------------- 1 | using LibraryA; 2 | 3 | namespace EntryPoint; 4 | 5 | public class Program 6 | { 7 | public static void Main() 8 | { 9 | Console.WriteLine("Hello from Entry Point"); 10 | A.SayHello(); 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ComplexSolution/LibraryA/A.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LibraryB; 3 | 4 | namespace LibraryA 5 | { 6 | public class A 7 | { 8 | public static void SayHello() 9 | { 10 | Console.WriteLine("Hello from Library A"); 11 | B.SayHello(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ComplexSolution/LibraryA/LibraryA.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ComplexSolution/LibraryB/B.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace LibraryB 5 | { 6 | public static class B 7 | { 8 | public static void SayHello() => 9 | Console.WriteLine( 10 | JsonConvert.DeserializeObject("\"Hello from Library B\"") 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ComplexSolution/LibraryB/LibraryB.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/Config.rsp: -------------------------------------------------------------------------------- 1 | # Here's a comment 2 | --using System.Text 3 | 4 | # Here's another comment 5 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoLibrary.README.txt: -------------------------------------------------------------------------------- 1 | DemoLibrary is used for #r unit tests. It contains a single class with the following source code: 2 | 3 | 4 | using System; 5 | 6 | namespace DemoLibrary 7 | { 8 | public static class DemoClass 9 | { 10 | public static int Multiply(int a, int b) => a * b; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoLibrary.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waf/CSharpRepl/bd79130d49c06736a2d5f4d56ac7643889ad2328/CSharpRepl.Tests/Data/DemoLibrary.dll -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoSolution/DemoSolution.DemoProject1/DemoClass1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DemoSolution.DemoProject1 4 | { 5 | public class DemoClass1 6 | { 7 | public int Add(int a, int b) => a + b; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoSolution/DemoSolution.DemoProject1/DemoSolution.DemoProject1.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoSolution/DemoSolution.DemoProject2/DemoClass2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DemoSolution.DemoProject2 4 | { 5 | public class DemoClass2 6 | { 7 | public int Add(int a, int b) => a + b; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoSolution/DemoSolution.DemoProject2/DemoSolution.DemoProject2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoSolution/DemoSolution.DemoProject3/DemoClass3.cs: -------------------------------------------------------------------------------- 1 | namespace DemoSolution.DemoProject3 2 | { 3 | public static class DemoClass3 4 | { 5 | public static void Main() { } 6 | 7 | public static string GetSystemManagementPath() 8 | => typeof(System.Management.ConnectionOptions).Assembly.Location; 9 | } 10 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoSolution/DemoSolution.DemoProject3/DemoSolution.DemoProject3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/DemoSolution/DemoSolution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32228.430 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoSolution.DemoProject1", "DemoSolution.DemoProject1\DemoSolution.DemoProject1.csproj", "{7E5196C3-33EB-4414-8289-1437EBAAC5AB}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoSolution.DemoProject2", "DemoSolution.DemoProject2\DemoSolution.DemoProject2.csproj", "{2378794F-E814-4B2B-91C0-7FFE41C1A2B1}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {7E5196C3-33EB-4414-8289-1437EBAAC5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {7E5196C3-33EB-4414-8289-1437EBAAC5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {7E5196C3-33EB-4414-8289-1437EBAAC5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {7E5196C3-33EB-4414-8289-1437EBAAC5AB}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2378794F-E814-4B2B-91C0-7FFE41C1A2B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2378794F-E814-4B2B-91C0-7FFE41C1A2B1}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2378794F-E814-4B2B-91C0-7FFE41C1A2B1}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2378794F-E814-4B2B-91C0-7FFE41C1A2B1}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {D58EC23D-FF84-4825-AC3E-78F9B787C0AC} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/Disassembly/TopLevelProgram.Input.txt: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | var x = 5; 4 | var y = 3; 5 | Console.WriteLine(x + y); -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/Disassembly/TopLevelProgram.Output.Debug.il: -------------------------------------------------------------------------------- 1 | // Method begins at RVA 0x2050 2 | // Header size: 12 3 | // Code size: 14 (0xe) 4 | .maxstack 2 5 | .entrypoint 6 | .locals init ( 7 | [0] int32, 8 | [1] int32 9 | ) 10 | 11 | IL_0000: ldc.i4.5 12 | IL_0001: stloc.0 13 | IL_0002: ldc.i4.3 14 | IL_0003: stloc.1 15 | IL_0004: ldloc.0 16 | IL_0005: ldloc.1 17 | IL_0006: add 18 | IL_0007: call void [System.Console]System.Console::WriteLine(int32) 19 | IL_000c: nop 20 | IL_000d: ret 21 | // Disassembled in Debug Mode. Press Ctrl+F9 to disassemble in Release Mode. 22 | // Compiling code as Console Application (with top-level statements): succeeded. 23 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/Disassembly/TopLevelProgram.Output.Release.il: -------------------------------------------------------------------------------- 1 | // Method begins at RVA 0x2050 2 | // Header size: 12 3 | // Code size: 11 (0xb) 4 | .maxstack 2 5 | .entrypoint 6 | .locals init ( 7 | [0] int32 8 | ) 9 | 10 | IL_0000: ldc.i4.5 11 | IL_0001: ldc.i4.3 12 | IL_0002: stloc.0 13 | IL_0003: ldloc.0 14 | IL_0004: add 15 | IL_0005: call void [System.Console]System.Console::WriteLine(int32) 16 | IL_000a: ret 17 | // Disassembled in Release Mode. 18 | // Compiling code as Console Application (with top-level statements): succeeded. 19 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/Disassembly/TypeDeclaration.Input.txt: -------------------------------------------------------------------------------- 1 | class Panda 2 | { 3 | public string Name { get; init; } 4 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/Disassembly/TypeDeclaration.Output.Debug.il: -------------------------------------------------------------------------------- 1 | .class private auto ansi '' 2 | { 3 | } // end of class 4 | 5 | .class private auto ansi beforefieldinit Panda 6 | extends [System.Runtime]System.Object 7 | { 8 | // Fields 9 | .field private initonly string 'k__BackingField' 10 | .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 11 | 01 00 00 00 12 | ) 13 | .custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ( 14 | 01 00 00 00 00 00 00 00 15 | ) 16 | 17 | // Methods 18 | .method public hidebysig specialname 19 | instance string get_Name () cil managed 20 | { 21 | .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 22 | 01 00 00 00 23 | ) 24 | // Method begins at RVA 0x2050 25 | // Header size: 1 26 | // Code size: 7 (0x7) 27 | .maxstack 8 28 | 29 | IL_0000: ldarg.0 30 | IL_0001: ldfld string Panda::'k__BackingField' 31 | IL_0006: ret 32 | } // end of method Panda::get_Name 33 | 34 | .method public hidebysig specialname 35 | instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Name ( 36 | string 'value' 37 | ) cil managed 38 | { 39 | .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 40 | 01 00 00 00 41 | ) 42 | // Method begins at RVA 0x2058 43 | // Header size: 1 44 | // Code size: 8 (0x8) 45 | .maxstack 8 46 | 47 | IL_0000: ldarg.0 48 | IL_0001: ldarg.1 49 | IL_0002: stfld string Panda::'k__BackingField' 50 | IL_0007: ret 51 | } // end of method Panda::set_Name 52 | 53 | .method public hidebysig specialname rtspecialname 54 | instance void .ctor () cil managed 55 | { 56 | // Method begins at RVA 0x2061 57 | // Header size: 1 58 | // Code size: 8 (0x8) 59 | .maxstack 8 60 | 61 | IL_0000: ldarg.0 62 | IL_0001: call instance void [System.Runtime]System.Object::.ctor() 63 | IL_0006: nop 64 | IL_0007: ret 65 | } // end of method Panda::.ctor 66 | 67 | // Properties 68 | .property instance string Name() 69 | { 70 | .get instance string Panda::get_Name() 71 | .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) Panda::set_Name(string) 72 | } 73 | 74 | } // end of class Panda 75 | 76 | // Disassembled in Debug Mode. Press Ctrl+F9 to disassemble in Release Mode. 77 | // Compiling code as Console Application (with top-level statements): failed. 78 | // - Program does not contain a static 'Main' method suitable for an entry point 79 | // Compiling code as DLL: succeeded. 80 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/Disassembly/TypeDeclaration.Output.Release.il: -------------------------------------------------------------------------------- 1 | .class private auto ansi '' 2 | { 3 | } // end of class 4 | 5 | .class private auto ansi beforefieldinit Panda 6 | extends [System.Runtime]System.Object 7 | { 8 | // Fields 9 | .field private initonly string 'k__BackingField' 10 | .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 11 | 01 00 00 00 12 | ) 13 | 14 | // Methods 15 | .method public hidebysig specialname 16 | instance string get_Name () cil managed 17 | { 18 | .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 19 | 01 00 00 00 20 | ) 21 | // Method begins at RVA 0x2050 22 | // Header size: 1 23 | // Code size: 7 (0x7) 24 | .maxstack 8 25 | 26 | IL_0000: ldarg.0 27 | IL_0001: ldfld string Panda::'k__BackingField' 28 | IL_0006: ret 29 | } // end of method Panda::get_Name 30 | 31 | .method public hidebysig specialname 32 | instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Name ( 33 | string 'value' 34 | ) cil managed 35 | { 36 | .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 37 | 01 00 00 00 38 | ) 39 | // Method begins at RVA 0x2058 40 | // Header size: 1 41 | // Code size: 8 (0x8) 42 | .maxstack 8 43 | 44 | IL_0000: ldarg.0 45 | IL_0001: ldarg.1 46 | IL_0002: stfld string Panda::'k__BackingField' 47 | IL_0007: ret 48 | } // end of method Panda::set_Name 49 | 50 | .method public hidebysig specialname rtspecialname 51 | instance void .ctor () cil managed 52 | { 53 | // Method begins at RVA 0x2061 54 | // Header size: 1 55 | // Code size: 7 (0x7) 56 | .maxstack 8 57 | 58 | IL_0000: ldarg.0 59 | IL_0001: call instance void [System.Runtime]System.Object::.ctor() 60 | IL_0006: ret 61 | } // end of method Panda::.ctor 62 | 63 | // Properties 64 | .property instance string Name() 65 | { 66 | .get instance string Panda::get_Name() 67 | .set instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) Panda::set_Name(string) 68 | } 69 | 70 | } // end of class Panda 71 | 72 | // Disassembled in Release Mode. 73 | // Compiling code as Console Application (with top-level statements): failed. 74 | // - Program does not contain a static 'Main' method suitable for an entry point 75 | // Compiling code as DLL: succeeded. 76 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/LoadScript.csx: -------------------------------------------------------------------------------- 1 | Console.WriteLine("Hello World!"); -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/ResponseFile.rsp: -------------------------------------------------------------------------------- 1 | # C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies\CSharpInteractive.rsp 2 | /r:System 3 | /r:System.ValueTuple.dll 4 | /u:System 5 | #/u:System.Console 6 | /u:System.Linq 7 | 8 | # more references 9 | /r:Foo.Main.Logic.dll 10 | /r:lib.dll 11 | /u:Foo.Main.Text 12 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/WebApplication1.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waf/CSharpRepl/bd79130d49c06736a2d5f4d56ac7643889ad2328/CSharpRepl.Tests/Data/WebApplication1.dll -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/WebApplication1.runtimeconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtimeOptions": { 3 | "tfm": "net6.0", 4 | "framework": { 5 | "name": "Microsoft.AspNetCore.App", 6 | "version": "6.0.0" 7 | }, 8 | "configProperties": { 9 | "System.GC.Server": true, 10 | "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/Data/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "syntaxHighlightingColors": [ 3 | { 4 | "name": "class name", 5 | "foreground": "BrightMagenta" 6 | }, 7 | { 8 | "name": "struct name", 9 | "foreground": "Yellow" 10 | }, 11 | { 12 | "name": "delegate name", 13 | "foreground": "#8BE9FD" 14 | }, 15 | { 16 | "name": "interface name", 17 | "foreground": "#8BE9FD" 18 | }, 19 | { 20 | "name": "module name", 21 | "foreground": "#8BE9FD" 22 | }, 23 | { 24 | "name": "record class name", 25 | "foreground": "#8BE9FD" 26 | }, 27 | { 28 | "name": "record struct name", 29 | "foreground": "#8BE9FD" 30 | }, 31 | { 32 | "name": "enum name", 33 | "foreground": "#50FA7B" 34 | }, 35 | { 36 | "name": "constant name", 37 | "foreground": "#F8F8F2" 38 | }, 39 | { 40 | "name": "enum member name", 41 | "foreground": "#F8F8F2" 42 | }, 43 | { 44 | "name": "event name", 45 | "foreground": "#F8F8F2" 46 | }, 47 | { 48 | "name": "extension method name", 49 | "foreground": "#F8F8F2" 50 | }, 51 | { 52 | "name": "text", 53 | "foreground": "#F8F8F2" 54 | }, 55 | { 56 | "name": "identifier", 57 | "foreground": "#F8F8F2" 58 | }, 59 | { 60 | "name": "label name", 61 | "foreground": "#F8F8F2" 62 | }, 63 | { 64 | "name": "local name", 65 | "foreground": "#F8F8F2" 66 | }, 67 | { 68 | "name": "method name", 69 | "foreground": "#F8F8F2" 70 | }, 71 | { 72 | "name": "property name", 73 | "foreground": "#F8F8F2" 74 | }, 75 | { 76 | "name": "namespace name", 77 | "foreground": "#F8F8F2" 78 | }, 79 | { 80 | "name": "parameter name", 81 | "foreground": "#F8F8F2" 82 | }, 83 | { 84 | "name": "number", 85 | "foreground": "#BD93F9" 86 | }, 87 | { 88 | "name": "keyword - control", 89 | "foreground": "#FF79C6" 90 | }, 91 | { 92 | "name": "keyword", 93 | "foreground": "#FF79C6" 94 | }, 95 | { 96 | "name": "operator", 97 | "foreground": "#FF79C6" 98 | }, 99 | { 100 | "name": "operator - overloaded", 101 | "foreground": "#FF79C6" 102 | }, 103 | { 104 | "name": "preprocessor keyword", 105 | "foreground": "#FF79C6" 106 | }, 107 | { 108 | "name": "string - escape character", 109 | "foreground": "#FF79C6" 110 | }, 111 | { 112 | "name": "string - verbatim", 113 | "foreground": "#F1FA8C" 114 | }, 115 | { 116 | "name": "string", 117 | "foreground": "#F1FA8C" 118 | }, 119 | { 120 | "name": "type parameter name", 121 | "foreground": "#FFB86C" 122 | }, 123 | { 124 | "name": "comment", 125 | "foreground": "#6272A4" 126 | }, 127 | { 128 | "name": "xml doc comment - attribute quotes", 129 | "foreground": "#50FA7B" 130 | }, 131 | { 132 | "name": "xml doc comment - attribute value", 133 | "foreground": "#50FA7B" 134 | }, 135 | { 136 | "name": "xml doc comment - attribute name", 137 | "foreground": "#6272A4" 138 | }, 139 | { 140 | "name": "xml doc comment - cdata section", 141 | "foreground": "#6272A4" 142 | }, 143 | { 144 | "name": "xml doc comment - comment", 145 | "foreground": "#6272A4" 146 | }, 147 | { 148 | "name": "xml doc comment - delimiter", 149 | "foreground": "#6272A4" 150 | }, 151 | { 152 | "name": "xml doc comment - entity reference", 153 | "foreground": "#6272A4" 154 | }, 155 | { 156 | "name": "xml doc comment - name", 157 | "foreground": "#6272A4" 158 | }, 159 | { 160 | "name": "xml doc comment - processing instruction", 161 | "foreground": "#6272A4" 162 | }, 163 | { 164 | "name": "xml doc comment - text", 165 | "foreground": "#6272A4" 166 | } 167 | ] 168 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/DisassemblerTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using CSharpRepl.Services; 4 | using CSharpRepl.Services.Roslyn; 5 | using CSharpRepl.Services.Roslyn.Scripting; 6 | using Microsoft.CodeAnalysis; 7 | using NSubstitute; 8 | using Xunit; 9 | 10 | namespace CSharpRepl.Tests; 11 | 12 | [Collection(nameof(RoslynServices))] 13 | public class DisassemblerTests : IAsyncLifetime 14 | { 15 | private readonly RoslynServices services; 16 | 17 | public DisassemblerTests() 18 | { 19 | var console = Substitute.For(); 20 | console.PrettyPromptConsole.BufferWidth.Returns(200); 21 | this.services = new RoslynServices(console, new Configuration(), new TestTraceLogger()); 22 | } 23 | 24 | public Task InitializeAsync() => services.WarmUpAsync([]); 25 | public Task DisposeAsync() => Task.CompletedTask; 26 | 27 | [Theory] 28 | [InlineData(OptimizationLevel.Debug, "TopLevelProgram")] 29 | [InlineData(OptimizationLevel.Release, "TopLevelProgram")] 30 | [InlineData(OptimizationLevel.Debug, "TypeDeclaration")] 31 | [InlineData(OptimizationLevel.Release, "TypeDeclaration")] 32 | public async Task Disassemble_InputCSharp_OutputILAsync(OptimizationLevel optimizationLevel, string testCase) 33 | { 34 | var input = File.ReadAllText($"./Data/Disassembly/{testCase}.Input.txt").Replace("\r\n", "\n"); 35 | var expectedOutput = File.ReadAllText($"./Data/Disassembly/{testCase}.Output.{optimizationLevel}.il").Replace("\r\n", "\n").Trim(); 36 | 37 | var result = await services.ConvertToIntermediateLanguage(input, debugMode: optimizationLevel == OptimizationLevel.Debug); 38 | var actualOutput = Assert 39 | .IsType(result) 40 | .ReturnValue 41 | .ToString(); 42 | 43 | Assert.Equal(expectedOutput, actualOutput); 44 | } 45 | 46 | [Fact] 47 | public async Task Disassemble_ImportsAcrossMultipleReplLines_CanDisassemble() 48 | { 49 | // import a namespace 50 | await services.EvaluateAsync("using System.Globalization;"); 51 | 52 | // disassemble code that uses the above imported namespace. 53 | var result = await services.ConvertToIntermediateLanguage("var x = CultureInfo.CurrentCulture;", debugMode: false); 54 | 55 | var success = Assert.IsType(result); 56 | Assert.Contains("Compiling code as Console Application (with top-level statements): succeeded", success.ReturnValue.ToString()); 57 | } 58 | 59 | [Fact] 60 | public async Task Disassemble_InputAcrossMultipleReplLines_CanDisassemble() 61 | { 62 | // define a variable 63 | await services.EvaluateAsync("var x = 5;"); 64 | 65 | // disassemble code that uses the above variable. This is an interesting case as the roslyn scripting will convert 66 | // the above local variable into a field, so it can be referenced by a subsequent script. 67 | var result = await services.ConvertToIntermediateLanguage("Console.WriteLine(x)", debugMode: false); 68 | 69 | var success = Assert.IsType(result); 70 | Assert.Contains("Compiling code as Scripting session (will be overly verbose): succeeded", success.ReturnValue.ToString()); 71 | } 72 | 73 | /// 74 | /// https://github.com/waf/CSharpRepl/issues/208 75 | /// 76 | [Fact] 77 | public async Task Disassemble_Empty() 78 | { 79 | // import a namespace 80 | await services.EvaluateAsync("using System;"); 81 | 82 | // disassemble code that uses the above imported namespace. 83 | var result = await services.ConvertToIntermediateLanguage("", debugMode: false); 84 | 85 | var success = Assert.IsType(result); 86 | var resultText = success.ReturnValue.ToString(); 87 | Assert.Contains(".class private auto ansi ''", resultText); 88 | Assert.Contains("// end of class ", resultText); 89 | Assert.Contains("// Disassembled in Release Mode", resultText); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace CSharpRepl.Tests; 4 | 5 | internal static class Extensions 6 | { 7 | private static readonly Regex AnsiEscapeCodeRegex = new(@"\u001b\[.+?m"); 8 | 9 | public static string RemoveFormatting(this string input) => AnsiEscapeCodeRegex.Replace(input, ""); 10 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/FakeHttp.cs: -------------------------------------------------------------------------------- 1 | #region License Header 2 | // This Source Code Form is subject to the terms of the Mozilla Public 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | #endregion 6 | 7 | using System; 8 | using System.Collections; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Net; 12 | using System.Net.Http; 13 | using System.Text; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | namespace CSharpRepl.Tests; 18 | 19 | using RequestMatcher = Func; 20 | 21 | internal class FakeHttp : HttpMessageHandler, IEnumerable<(RequestMatcher, (HttpStatusCode, string))> 22 | { 23 | private List<(RequestMatcher requestMatcher, (HttpStatusCode status, string content) response)> mockResponses = []; 24 | 25 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 26 | { 27 | var content = await request.Content.ReadAsStringAsync(cancellationToken); 28 | var response = mockResponses.First(r => r.requestMatcher(content)).response; 29 | return new HttpResponseMessage(response.status) 30 | { 31 | Content = new StringContent(response.content, Encoding.UTF8, "application/json") 32 | }; 33 | } 34 | 35 | public void Add(RequestMatcher requestMatcher, HttpStatusCode statusCode, string content) 36 | { 37 | mockResponses.Add((requestMatcher, (statusCode, content))); 38 | } 39 | 40 | IEnumerator IEnumerable.GetEnumerator() => 41 | ((IEnumerable)mockResponses).GetEnumerator(); 42 | 43 | public IEnumerator<(RequestMatcher, (HttpStatusCode, string))> GetEnumerator() => 44 | ((IEnumerable<(RequestMatcher, (HttpStatusCode, string))>)mockResponses).GetEnumerator(); 45 | } 46 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/FormattedStringParserTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Collections.Generic; 6 | using CSharpRepl.Services.Theming; 7 | using PrettyPrompt.Highlighting; 8 | using Xunit; 9 | 10 | namespace CSharpRepl.Tests; 11 | 12 | public class FormattedStringParserTests 13 | { 14 | [Theory] 15 | [InlineData("", "")] 16 | [InlineData(" ", " ")] 17 | [InlineData("abc", "abc")] 18 | [InlineData("abc def", "abc def")] 19 | [InlineData("ab(cd)", "ab(cd)")] 20 | [InlineData("[[", "[")] 21 | [InlineData("ab[[cd]]", "ab[cd]")] 22 | [InlineData("/", "/")] 23 | public void ParseNonFormatted(string pattern, string expectedResult) 24 | { 25 | Assert.True(FormattedStringParser.TryParse(pattern, out var result)); 26 | Assert.Equal(0, result.FormatSpans.Length); 27 | Assert.Equal(expectedResult, result.Text); 28 | } 29 | 30 | [Theory] 31 | [InlineData("[")] 32 | [InlineData("]")] 33 | [InlineData("[]")] 34 | [InlineData("[a")] 35 | [InlineData("]a")] 36 | [InlineData("[]a")] 37 | [InlineData("a[")] 38 | [InlineData("a]")] 39 | [InlineData("a[]")] 40 | [InlineData("[red][blue]a")] 41 | [InlineData("[red]a[blue]")] 42 | [InlineData("ab[[cd]")] 43 | [InlineData("ab[cd]]")] 44 | [InlineData("[red]")] 45 | [InlineData("[red]a")] 46 | [InlineData("[/]")] 47 | [InlineData("a[/]")] 48 | [InlineData("[red][/][/]")] 49 | [InlineData("[on][/]")] 50 | [InlineData("[red on][/]")] 51 | [InlineData("[on on red][/]")] 52 | public void ParseBroken(string pattern) 53 | { 54 | Assert.False(FormattedStringParser.TryParse(pattern, out var result)); 55 | Assert.Equal(0, result.FormatSpans.Length); 56 | Assert.True(result.IsEmpty); 57 | } 58 | 59 | [Theory] 60 | [MemberData(nameof(ParseStyleData))] 61 | public void ParseStyle(string pattern, FormattedString expectedResult) 62 | { 63 | Assert.Equal(expectedResult, FormattedStringParser.Parse(pattern)); 64 | } 65 | 66 | public static IEnumerable ParseStyleData 67 | { 68 | get 69 | { 70 | yield return new object[] { "[red][/]", FormattedString.Empty }; 71 | yield return new object[] { "[red]a[/]", new FormattedString("a", new FormatSpan(0, 1, AnsiColor.Red)) }; 72 | yield return new object[] { "[red]a[/]b", new FormattedString("ab", new FormatSpan(0, 1, AnsiColor.Red)) }; 73 | yield return new object[] { "[red]a[/][green]b[/]", new FormattedString("ab", new FormatSpan(0, 1, AnsiColor.Red), new FormatSpan(1, 1, AnsiColor.Green)) }; 74 | 75 | yield return new object[] { "[red bold]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Foreground: AnsiColor.Red, Bold: true))) }; 76 | yield return new object[] { "[red underline]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Foreground: AnsiColor.Red, Underline: true))) }; 77 | yield return new object[] { "[red inverted]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Foreground: AnsiColor.Red, Inverted: true))) }; 78 | yield return new object[] { "[red bold underline inverted]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Foreground: AnsiColor.Red, Bold: true, Underline: true, Inverted: true))) }; 79 | 80 | yield return new object[] { "[on red]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Background: AnsiColor.Red))) }; 81 | yield return new object[] { "[blue on red]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Foreground: AnsiColor.Blue, Background: AnsiColor.Red))) }; 82 | yield return new object[] { "[bold blue on red]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Foreground: AnsiColor.Blue, Background: AnsiColor.Red, Bold: true))) }; 83 | yield return new object[] { "[blue on red bold]a[/]", new FormattedString("a", new FormatSpan(0, 1, new ConsoleFormat(Foreground: AnsiColor.Blue, Background: AnsiColor.Red, Bold: true))) }; 84 | yield return new object[] { "[red]a[/][on green]b[/]", new FormattedString("ab", new FormatSpan(0, 1, AnsiColor.Red), new FormatSpan(1, 1, new ConsoleFormat(Background: AnsiColor.Green))) }; 85 | 86 | yield return new object[] { "[red][[a]][/][on green][[b]][/]", new FormattedString("[a][b]", new FormatSpan(0, 3, AnsiColor.Red), new FormatSpan(3, 3, new ConsoleFormat(Background: AnsiColor.Green))) }; 87 | yield return new object[] { "[bold]Usage[/]: [[OPTIONS]]", new FormattedString("Usage: [OPTIONS]", new FormatSpan(0, 5, new ConsoleFormat(Bold: true))) }; 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/NugetPackageInstallerTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using CSharpRepl.Services; 10 | using CSharpRepl.Services.Nuget; 11 | using CSharpRepl.Services.Roslyn; 12 | using Microsoft.CodeAnalysis; 13 | using Xunit; 14 | 15 | namespace CSharpRepl.Tests; 16 | 17 | [Collection(nameof(RoslynServices))] 18 | public class NugetPackageInstallerTests : IAsyncLifetime 19 | { 20 | private readonly NugetPackageInstaller installer; 21 | 22 | public NugetPackageInstallerTests() 23 | { 24 | installer = new NugetPackageInstaller(FakeConsole.CreateStubbedOutput().console, new Configuration()); 25 | } 26 | 27 | public Task InitializeAsync() => Task.CompletedTask; 28 | public Task DisposeAsync() => Task.CompletedTask; 29 | 30 | /// 31 | /// https://github.com/waf/CSharpRepl/issues/58 32 | /// 33 | [Fact] 34 | public async Task InstallRuntimeSpecificPackage() 35 | { 36 | var references = await installer.InstallAsync("System.Management", "6.0.0"); 37 | 38 | Assert.True(references.Length >= 1); 39 | var reference = references.FirstOrDefault(r => r.FilePath.EndsWith("System.Management.dll", StringComparison.OrdinalIgnoreCase)); 40 | Assert.NotNull(reference); 41 | 42 | var winRuntimeSelected = reference.FilePath.Contains(Path.Combine("runtimes", "win", "lib"), StringComparison.OrdinalIgnoreCase); 43 | var isWin = Environment.OSVersion.Platform == PlatformID.Win32NT; 44 | Assert.Equal(isWin, winRuntimeSelected); 45 | } 46 | 47 | [Fact] 48 | public async Task InstallPackageWithSupportedButEmptyTargets() 49 | { 50 | var references = await installer.InstallAsync("Microsoft.CSharp", "4.7.0"); //some targets contains only empty file '_._' 51 | 52 | Assert.True(references.Length >= 1); 53 | var reference = references.FirstOrDefault(r => r.FilePath.EndsWith("Microsoft.CSharp.dll", StringComparison.OrdinalIgnoreCase)); 54 | Assert.NotNull(reference); 55 | Assert.True(reference.FilePath.Contains("lib", StringComparison.OrdinalIgnoreCase)); 56 | } 57 | 58 | [Fact] 59 | public async Task InstallPackageThatOnlyContainsDependencies() 60 | { 61 | // humanizer does not target any frameworks itself, but depends on nuget packages that do. 62 | var references = await installer.InstallAsync("Humanizer", "2.14.1"); 63 | 64 | Assert.True(references.Length >= 1); 65 | var reference = references.FirstOrDefault(r => r.FilePath.EndsWith("Humanizer.dll", StringComparison.OrdinalIgnoreCase)); 66 | Assert.NotNull(reference); 67 | Assert.True(reference.FilePath.Contains("lib", StringComparison.OrdinalIgnoreCase)); 68 | } 69 | 70 | 71 | [Fact] // https://github.com/waf/CSharpRepl/issues/251 72 | public async Task InstallPackageThatContainsImplicitVersioning() 73 | { 74 | // The package version is "1.2" but if we accidentally normalize the version, it normalizes to 1.2.0 and causes directory not found errors 75 | var references = await installer.InstallAsync("Emik.Results", "1.2"); 76 | 77 | Assert.True(references.Length >= 1); 78 | var reference = references.FirstOrDefault(r => r.FilePath.EndsWith("Emik.Results.dll", StringComparison.OrdinalIgnoreCase)); 79 | Assert.NotNull(reference); 80 | Assert.True(reference.FilePath.Contains("lib", StringComparison.OrdinalIgnoreCase)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/ObjectFormatting/TestFormatter.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using CSharpRepl.Services; 4 | using CSharpRepl.Services.Roslyn.Formatting; 5 | using CSharpRepl.Services.Roslyn.Formatting.CustomObjectFormatters; 6 | using CSharpRepl.Services.SyntaxHighlighting; 7 | using CSharpRepl.Services.Theming; 8 | using Microsoft.Extensions.Caching.Memory; 9 | using Spectre.Console; 10 | using Xunit; 11 | 12 | namespace CSharpRepl.Tests.ObjectFormatting; 13 | 14 | internal class TestFormatter 15 | { 16 | private readonly IAnsiConsole console; 17 | private readonly SyntaxHighlighter highlighter; 18 | private readonly Configuration config; 19 | 20 | public TestFormatter(IAnsiConsole console, SyntaxHighlighter highlighter, Configuration config) 21 | { 22 | this.console = console; 23 | this.highlighter = highlighter; 24 | this.config = config; 25 | } 26 | 27 | public string Format(ICustomObjectFormatter formatter, object value, Level level) 28 | { 29 | var prettyPrompt = new PrettyPrinter(console, highlighter, config); 30 | Assert.True(formatter.IsApplicable(value)); 31 | return formatter.FormatToText(value, level, new Formatter(prettyPrompt, highlighter, console.Profile)).ToString(); 32 | } 33 | 34 | public static TestFormatter Create(IAnsiConsole console) => new( 35 | console, 36 | new SyntaxHighlighter( 37 | new MemoryCache(new MemoryCacheOptions()), 38 | new Theme(null, null, null, null, [])), 39 | new Configuration()); 40 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/PipedInputEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CSharpRepl.Services.Roslyn; 3 | using NSubstitute; 4 | using Xunit; 5 | 6 | namespace CSharpRepl.Tests; 7 | 8 | [Collection(nameof(RoslynServices))] 9 | public class PipedInputEvaluatorTests : IClassFixture 10 | { 11 | private readonly FakeConsoleAbstract console; 12 | private readonly RoslynServices roslyn; 13 | private readonly PipedInputEvaluator pipedInputEvaluator; 14 | 15 | public PipedInputEvaluatorTests(RoslynServicesFixture fixture) 16 | { 17 | this.console = fixture.ConsoleStub; 18 | this.roslyn = fixture.RoslynServices; 19 | this.pipedInputEvaluator = new PipedInputEvaluator(console, roslyn); 20 | } 21 | 22 | [Fact] 23 | public async Task EvaluateCollectedPipeInputAsync_FullyCollectsInput_ThenEvaluatesInput() 24 | { 25 | // verify we're collecting the input entirely before evaluating it. If we evaluated it line by line, 26 | // the following program would have an error because the if(false) { ... } would be evaluated as a 27 | // complete statement, and the "else" would be a syntax error. 28 | console.ReadLine().Returns( 29 | "if(false)", 30 | "{", 31 | " Console.WriteLine(\"true\");", 32 | "}", 33 | "else", 34 | "{", 35 | " Console.WriteLine(\"false\");", 36 | "}", 37 | null // end of piped input 38 | ); 39 | var result = await this.pipedInputEvaluator.EvaluateCollectedPipeInputAsync(); 40 | 41 | Assert.Equal(ExitCodes.Success, result); 42 | } 43 | 44 | [Fact] 45 | public async Task EvaluateStreamingPipeInputAsync_StreamsInput_EvaluatesCompleteStatements() 46 | { 47 | // in this mode, we want to read input line by line, group them into complete statements, and then 48 | // evaluate the complete statement. If we just did line-by-line then the below input would cause a 49 | // syntax error on the standalone `if(true)` 50 | console.ReadLine().Returns( 51 | "if(true)", 52 | "{", 53 | " Console.WriteLine(\"true\");", 54 | "}", 55 | "Console.WriteLine(\"false\");", 56 | null // end of piped input 57 | ); 58 | var result = await this.pipedInputEvaluator.EvaluateStreamingPipeInputAsync(); 59 | 60 | Assert.Equal(ExitCodes.Success, result); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/ProgramTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace CSharpRepl.Tests; 8 | 9 | public class ProgramTests 10 | { 11 | [Fact] 12 | public async Task MainMethod_Help_ShowsHelp() 13 | { 14 | using var outputCollector = OutputCollector.Capture(out var capturedOutput); 15 | await Program.Main(["-h"]); 16 | var output = capturedOutput.ToString(); 17 | output = output.RemoveFormatting(); 18 | 19 | Assert.Contains("Starts a REPL (read eval print loop) according to the provided [OPTIONS].", output); 20 | // should show default shared framework 21 | Assert.Contains("Microsoft.NETCore.App (default)", output); 22 | } 23 | 24 | [Fact] 25 | public async Task MainMethod_Version_ShowsVersion() 26 | { 27 | using var outputCollector = OutputCollector.Capture(out var capturedOutput); 28 | 29 | await Program.Main(["-v"]); 30 | 31 | var output = capturedOutput.ToString(); 32 | output = output.RemoveFormatting().Split("+")[0]; // remove formatting and trailing git SHA 33 | Assert.StartsWith("C# REPL", output); 34 | var version = new Version(output.Trim("C# REPL-rc-alpha-beta\r\n".ToCharArray())); 35 | Assert.True(version.Major + version.Minor > 0); 36 | } 37 | 38 | [Fact] 39 | public async Task MainMethod_CannotParse_DoesNotThrow() 40 | { 41 | using var outputCollector = OutputCollector.Capture(out _, out var capturedError); 42 | 43 | await Program.Main(["bonk"]); 44 | 45 | var error = capturedError.ToString(); 46 | Assert.Equal( 47 | "Unrecognized command or argument 'bonk'." + Environment.NewLine, 48 | error 49 | ); 50 | } 51 | } 52 | 53 | /// 54 | /// Captures standard output. Because there's only one Console.Out, 55 | /// this forces single threaded execution of unit tests that use it. 56 | /// 57 | public sealed class OutputCollector : IDisposable 58 | { 59 | private static readonly Semaphore semaphore = new(1, 1); 60 | 61 | private readonly TextWriter normalStandardOutput; 62 | private readonly TextWriter normalStandardError; 63 | private readonly StringWriter fakeConsoleOutput; 64 | private readonly StringWriter fakeConsoleError; 65 | 66 | private OutputCollector() 67 | { 68 | normalStandardOutput = Console.Out; 69 | normalStandardError = Console.Error; 70 | fakeConsoleOutput = new StringWriter(); 71 | fakeConsoleError = new StringWriter(); 72 | Console.SetOut(fakeConsoleOutput); 73 | Console.SetError(fakeConsoleError); 74 | } 75 | 76 | public static OutputCollector Capture(out StringWriter capturedOutput) 77 | { 78 | semaphore.WaitOne(); 79 | 80 | var outputCollector = new OutputCollector(); 81 | capturedOutput = outputCollector.fakeConsoleOutput; 82 | return outputCollector; 83 | } 84 | 85 | public static OutputCollector Capture(out StringWriter capturedOutput, out StringWriter capturedError) 86 | { 87 | semaphore.WaitOne(); 88 | 89 | var outputCollector = new OutputCollector(); 90 | capturedOutput = outputCollector.fakeConsoleOutput; 91 | capturedError = outputCollector.fakeConsoleError; 92 | return outputCollector; 93 | } 94 | 95 | public void Dispose() 96 | { 97 | Console.SetOut(normalStandardOutput); 98 | Console.SetOut(normalStandardError); 99 | semaphore.Release(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/PromptConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using CSharpRepl.PrettyPromptConfig; 7 | using CSharpRepl.Services; 8 | using CSharpRepl.Services.Roslyn; 9 | using PrettyPrompt; 10 | using PrettyPrompt.Consoles; 11 | using Xunit; 12 | 13 | namespace CSharpRepl.Tests; 14 | 15 | [Collection(nameof(RoslynServices))] 16 | public class PromptConfigurationTests : IAsyncLifetime 17 | { 18 | private readonly RoslynServices services; 19 | private readonly IConsoleEx console; 20 | private readonly StringBuilder stdout; 21 | 22 | public PromptConfigurationTests() 23 | { 24 | var (console, stdout) = FakeConsole.CreateStubbedOutput(); 25 | this.console = console; 26 | this.stdout = stdout; 27 | 28 | this.services = new RoslynServices(console, new Configuration(), new TestTraceLogger()); 29 | } 30 | 31 | public Task InitializeAsync() => services.WarmUpAsync([]); 32 | public Task DisposeAsync() => Task.CompletedTask; 33 | 34 | [Theory] 35 | [MemberData(nameof(KeyPresses))] 36 | public void PromptConfiguration_CanCreate(ConsoleKeyInfo keyInfo) 37 | { 38 | IPromptCallbacks configuration = new CSharpReplPromptCallbacks(console, services, new Configuration()); 39 | Assert.True(configuration.TryGetKeyPressCallbacks(keyInfo, out var callback)); 40 | callback.Invoke("Console.WriteLine(\"Hi!\");", 0, default); 41 | } 42 | 43 | [Theory] 44 | [InlineData(true)] 45 | [InlineData(false)] 46 | public async Task PromptConfiguration_Identation(bool shiftPressed) 47 | { 48 | IPromptCallbacks configuration = new CSharpReplPromptCallbacks(console, services, new Configuration()); 49 | var enterKey = new KeyPress(new ConsoleKeyInfo('\0', ConsoleKey.Enter, shift: shiftPressed, alt: false, control: false)); 50 | 51 | var transformed = await configuration.TransformKeyPressAsync("if (true) {", 11, enterKey, CancellationToken.None); 52 | Assert.Equal("\n\t", transformed.PastedText); 53 | } 54 | 55 | public static IEnumerable KeyPresses() 56 | { 57 | yield return new object[] { new ConsoleKeyInfo('\0', ConsoleKey.F1, shift: false, alt: false, control: false) }; 58 | yield return new object[] { new ConsoleKeyInfo('\0', ConsoleKey.F1, shift: false, alt: false, control: true) }; 59 | yield return new object[] { new ConsoleKeyInfo('\0', ConsoleKey.F9, shift: false, alt: false, control: false) }; 60 | yield return new object[] { new ConsoleKeyInfo('\0', ConsoleKey.F9, shift: false, alt: false, control: true) }; 61 | yield return new object[] { new ConsoleKeyInfo('\0', ConsoleKey.F12, shift: false, alt: false, control: false) }; 62 | yield return new object[] { new ConsoleKeyInfo('\0', ConsoleKey.D, shift: false, alt: false, control: true) }; 63 | } 64 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/RoslynServicesFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading.Tasks; 3 | using CSharpRepl.Services; 4 | using CSharpRepl.Services.Roslyn; 5 | using NSubstitute; 6 | using PrettyPrompt; 7 | using Xunit; 8 | 9 | namespace CSharpRepl.Tests; 10 | 11 | public sealed class RoslynServicesFixture : IAsyncLifetime 12 | { 13 | public FakeConsoleAbstract ConsoleStub { get; } 14 | public StringBuilder CapturedConsoleOutput { get; } 15 | public StringBuilder CapturedConsoleError { get; } 16 | public IPrompt PromptStub { get; } 17 | public RoslynServices RoslynServices { get; } 18 | 19 | public RoslynServicesFixture() 20 | { 21 | (this.ConsoleStub, this.CapturedConsoleOutput, this.CapturedConsoleError) = FakeConsole.CreateStubbedOutputAndError(); 22 | this.PromptStub = Substitute.For(); 23 | this.RoslynServices = new RoslynServices(ConsoleStub, new Configuration(), new TestTraceLogger()); 24 | } 25 | 26 | public Task DisposeAsync() => Task.CompletedTask; 27 | 28 | public Task InitializeAsync() => RoslynServices.WarmUpAsync([]); 29 | } 30 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/ScriptArgumentTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CSharpRepl.Services; 3 | using CSharpRepl.Services.Roslyn; 4 | using CSharpRepl.Services.Roslyn.Scripting; 5 | using Xunit; 6 | 7 | namespace CSharpRepl.Tests; 8 | 9 | [Collection(nameof(RoslynServices))] 10 | public class ScriptArgumentTests 11 | { 12 | [Theory] 13 | [InlineData("args[0]")] // array accessor 14 | [InlineData("Args[0]")] // IList accessor 15 | public async Task Evaluate_WithArguments_ArgumentsAvailable(string argsAccessor) 16 | { 17 | var (console, _) = FakeConsole.CreateStubbedOutput(); 18 | var services = new RoslynServices(console, new Configuration(), new TestTraceLogger()); 19 | var args = new[] { "Howdy" }; 20 | 21 | await services.WarmUpAsync(args); 22 | var variableAssignment = await services.EvaluateAsync($@"var x = {argsAccessor};"); 23 | var variableUsage = await services.EvaluateAsync(@"x"); 24 | 25 | Assert.IsType(variableAssignment); 26 | var usage = Assert.IsType(variableUsage); 27 | Assert.Equal("Howdy", usage.ReturnValue); 28 | } 29 | 30 | [Fact] 31 | public async Task Evaluate_PrettyPrint_PrintsPrettily() 32 | { 33 | var (console, stdOut) = FakeConsole.CreateStubbedOutput(); 34 | var services = new RoslynServices(console, new Configuration(), new TestTraceLogger()); 35 | 36 | await services.WarmUpAsync([]); 37 | _ = await services.EvaluateAsync("using System.Globalization;"); 38 | _ = await services.EvaluateAsync("CultureInfo.DefaultThreadCurrentCulture = new System.Globalization.CultureInfo(\"en-US\");"); 39 | var printStatement = await services.EvaluateAsync("Print(DateTime.MinValue)"); 40 | 41 | Assert.IsType(printStatement); 42 | Assert.Equal("[1/1/0001 12:00:00 AM]\n", console.AnsiConsole.Output); 43 | } 44 | } -------------------------------------------------------------------------------- /CSharpRepl.Tests/SymbolExplorerTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Threading.Tasks; 6 | using CSharpRepl.Services; 7 | using CSharpRepl.Services.Roslyn; 8 | using CSharpRepl.Services.SymbolExploration; 9 | using Xunit; 10 | 11 | namespace CSharpRepl.Tests; 12 | 13 | [Collection(nameof(RoslynServices))] 14 | public class SymbolExplorerTests : IAsyncLifetime 15 | { 16 | private readonly RoslynServices services; 17 | 18 | public SymbolExplorerTests() 19 | { 20 | var (console, _) = FakeConsole.CreateStubbedOutput(); 21 | this.services = new RoslynServices(console, new Configuration(), new TestTraceLogger()); 22 | } 23 | 24 | public Task InitializeAsync() => services.WarmUpAsync([]); 25 | public Task DisposeAsync() => Task.CompletedTask; 26 | 27 | [Fact] 28 | public async Task GetSymbolAtIndex_ReturnsFullyQualifiedName() 29 | { 30 | var symbol = await services.GetSymbolAtIndexAsync(@"Console.WriteLine(""howdy"")", "Console.Wri".Length); 31 | Assert.Equal("System.Console.WriteLine", symbol.SymbolDisplay); 32 | } 33 | 34 | [Fact] 35 | public async Task GetSymbolAtIndex_ClassInSourceLinkedAssembly_ReturnsSourceLinkUrl() 36 | { 37 | // should return a string like https://www.github.com/dotnet/runtime/blob/208e377a5329ad6eb1db5e5fb9d4590fa50beadd/src/libraries/System.Console/src/System/Console.cs 38 | var symbol = await services.GetSymbolAtIndexAsync(@"Console.WriteLine(""howdy"")", "Conso".Length); 39 | 40 | Assert.StartsWith("https://www.github.com/dotnet/runtime/", symbol.Url); 41 | Assert.EndsWith("Console.cs", symbol.Url); 42 | } 43 | 44 | [Fact] 45 | public async Task GetSymbolAtIndex_GenericTypeInSourceLinkedAssembly_ReturnsSourceLinkUrl() 46 | { 47 | // should return a string like https://www.github.com/dotnet/runtime/blob/1381d5ebd2ab1f292848d5b19b80cf71ac332508/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/List.cs 48 | var symbol = await services.GetSymbolAtIndexAsync(@"List", "Li".Length); 49 | 50 | Assert.StartsWith("https://www.github.com/dotnet/runtime/", symbol.Url); 51 | Assert.EndsWith("List.cs", symbol.Url); 52 | } 53 | 54 | [Fact] 55 | public async Task GetSymbolAtIndex_MethodInSourceLinkedAssembly_ReturnsSourceLinkUrl() 56 | { 57 | // should return a string like https://www.github.com/dotnet/runtime/blob/208e377a5329ad6eb1db5e5fb9d4590fa50beadd/src/libraries/System.Console/src/System/Console.cs#L635-L636 58 | var symbol = await services.GetSymbolAtIndexAsync(@"Console.WriteLine(""howdy"")", "Console.Wri".Length); 59 | 60 | AssertLinkWithLineNumber(symbol); 61 | } 62 | 63 | [Fact] 64 | public async Task GetSymbolAtIndex_PropertyInSourceLinkedAssembly_ReturnsSourceLinkUrl() 65 | { 66 | // should return a string like https://www.github.com/dotnet/runtime/blob/208e377a5329ad6eb1db5e5fb9d4590fa50beadd/src/libraries/System.Console/src/System/Console.cs 67 | var symbol = await services.GetSymbolAtIndexAsync(@"Console.Out", "Console.Ou".Length); 68 | 69 | AssertLinkWithLineNumber(symbol); 70 | } 71 | 72 | [Fact] 73 | public async Task GetSymbolAtIndex_EventInSourceLinkedAssembly_ReturnsSourceLinkUrl() 74 | { 75 | // should return a string like https://www.github.com/dotnet/runtime/blob/208e377a5329ad6eb1db5e5fb9d4590fa50beadd/src/libraries/System.Console/src/System/Console.cs 76 | var symbol = await services.GetSymbolAtIndexAsync(@"Console.CancelKeyPress", "Console.CancelKe".Length); 77 | 78 | AssertLinkWithLineNumber(symbol); 79 | } 80 | 81 | [Fact] 82 | public async Task GetSymbolAtIndex_InvalidSymbol_NoException() 83 | { 84 | var symbol = await services.GetSymbolAtIndexAsync(@"wow!", 2); 85 | 86 | Assert.Equal(SymbolResult.Unknown, symbol); 87 | } 88 | 89 | [Fact] 90 | public async Task GetSymbolAtIndex_NonSourceLinkedAssembly_NoException() 91 | { 92 | _ = await services.EvaluateAsync(@"#r ""./Data/DemoLibrary.dll"""); 93 | _ = await services.EvaluateAsync("using DemoLibrary;"); 94 | var symbol = await services.GetSymbolAtIndexAsync("DemoClass.Multiply", "DemoClass.Multi".Length); 95 | 96 | Assert.Equal("DemoLibrary.DemoClass.Multiply", symbol.SymbolDisplay); 97 | Assert.Null(symbol.Url); 98 | } 99 | 100 | private static void AssertLinkWithLineNumber(SymbolResult symbol) 101 | { 102 | var urlParts = symbol.Url.Split('#'); 103 | Assert.Equal(2, urlParts.Length); 104 | 105 | var url = urlParts[0]; 106 | Assert.StartsWith("https://www.github.com/dotnet/runtime/", url); 107 | 108 | var lineHash = urlParts[1]; 109 | const string LinePattern = "L[0-9]+"; 110 | Assert.Matches($"^{LinePattern}-{LinePattern}$", lineHash); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/SyntaxHighlightingTests.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using CSharpRepl.Services; 8 | using CSharpRepl.Services.Roslyn; 9 | using Microsoft.CodeAnalysis.Text; 10 | using Xunit; 11 | 12 | namespace CSharpRepl.Tests; 13 | 14 | [Collection(nameof(RoslynServices))] 15 | public class SyntaxHighlightingTests : IAsyncLifetime 16 | { 17 | private readonly RoslynServices services; 18 | 19 | public SyntaxHighlightingTests() 20 | { 21 | var (console, _) = FakeConsole.CreateStubbedOutput(); 22 | this.services = new RoslynServices(console, new Configuration( 23 | theme: "Data/theme.json" 24 | ), new TestTraceLogger()); 25 | } 26 | 27 | public Task InitializeAsync() => services.WarmUpAsync([]); 28 | public Task DisposeAsync() => Task.CompletedTask; 29 | 30 | [Fact] 31 | public async Task SyntaxHighlightAsync_GivenCode_DetectsTextSpans() 32 | { 33 | var highlighted = await services.SyntaxHighlightAsync(@"var foo = ""bar"";"); 34 | Assert.Equal(5, highlighted.Count); 35 | 36 | var expected = new TextSpan[] { 37 | new(0, 3), // var 38 | new(4, 3), // foo 39 | new(8, 1), // = 40 | new(10, 5),// "bar" 41 | new(15, 1) // ; 42 | }; 43 | 44 | Assert.Equal(expected, highlighted.Select(highlight => highlight.TextSpan)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CSharpRepl.Tests/TraceLoggerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using CSharpRepl.Logging; 5 | using CSharpRepl.Services.Logging; 6 | using Xunit; 7 | 8 | namespace CSharpRepl.Tests; 9 | 10 | /// 11 | /// more of an integration test than anything; we want to make sure the logger actually does log to a file. 12 | /// 13 | public class TraceLoggerTests 14 | { 15 | [Fact] 16 | public void Create_ThenLog_WritesToFile() 17 | { 18 | //TraceLogger writes to console and we need to eliminate collision between threads (https://github.com/waf/CSharpRepl/issues/307). 19 | using (OutputCollector.Capture(out _)) 20 | { 21 | var path = Path.GetTempFileName(); 22 | var logger = TraceLogger.Create(path); 23 | 24 | logger.Log("Hello World, I'm a hopeful and optimistic REPL."); 25 | logger.Log(() => "Arrgghh an error"); 26 | 27 | var loggedLines = File.ReadAllLines(path); 28 | Assert.Contains("Trace session starting", loggedLines[0]); 29 | Assert.Contains("Hello World, I'm a hopeful and optimistic REPL.", loggedLines[1]); 30 | Assert.Contains("Arrgghh an error", loggedLines[2]); 31 | } 32 | } 33 | 34 | [Fact] 35 | public void LogPaths_GivenPaths_GroupsByPrefix() 36 | { 37 | //TraceLogger writes to console and we need to eliminate collision between threads (https://github.com/waf/CSharpRepl/issues/307). 38 | using (OutputCollector.Capture(out _)) 39 | { 40 | var path = Path.GetTempFileName(); 41 | var logger = TraceLogger.Create(path); 42 | 43 | logger.LogPaths("Some Files", () => [@"/Foo/Bar.txt", @"/Foo/Baz.txt"]); 44 | 45 | var loggedLines = File.ReadAllLines(path); 46 | Assert.Contains("Trace session starting", loggedLines[0]); 47 | Assert.Contains(@"Some Files: ", loggedLines[1]); 48 | Assert.EndsWith(@"[""Bar.txt"", ""Baz.txt""]", loggedLines[1]); 49 | } 50 | } 51 | } 52 | 53 | /// 54 | /// Executes the delayed evaluation Funcs for testing purposes (to make sure they don't throw exceptions). 55 | /// 56 | public class TestTraceLogger : ITraceLogger 57 | { 58 | public void Log(string message) { } 59 | 60 | public void Log(Func message) { message(); } 61 | 62 | public void LogPaths(string message, Func> paths) => paths(); 63 | } 64 | -------------------------------------------------------------------------------- /CSharpRepl.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31410.414 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpRepl", "CSharpRepl\CSharpRepl.csproj", "{DAF35EEA-275E-4BF1-A7F0-5D2DF32FC0A5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpRepl.Services", "CSharpRepl.Services\CSharpRepl.Services.csproj", "{CF37FCD2-EF71-48F8-AE06-8A07D328F24A}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{000D6020-F5D7-4CC9-A539-36914452C63B}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | ARCHITECTURE.md = ARCHITECTURE.md 14 | nuget.config = nuget.config 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpRepl.Tests", "CSharpRepl.Tests\CSharpRepl.Tests.csproj", "{35584632-2D73-4767-B2C0-7DB1ABB2812E}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{00E474D3-ACA6-4210-8E9B-AD94ED920647}" 20 | ProjectSection(SolutionItems) = preProject 21 | .github\workflows\main.yml = .github\workflows\main.yml 22 | .github\workflows\release.yml = .github\workflows\release.yml 23 | EndProjectSection 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {DAF35EEA-275E-4BF1-A7F0-5D2DF32FC0A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {DAF35EEA-275E-4BF1-A7F0-5D2DF32FC0A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {DAF35EEA-275E-4BF1-A7F0-5D2DF32FC0A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {DAF35EEA-275E-4BF1-A7F0-5D2DF32FC0A5}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {CF37FCD2-EF71-48F8-AE06-8A07D328F24A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {CF37FCD2-EF71-48F8-AE06-8A07D328F24A}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {CF37FCD2-EF71-48F8-AE06-8A07D328F24A}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {CF37FCD2-EF71-48F8-AE06-8A07D328F24A}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {35584632-2D73-4767-B2C0-7DB1ABB2812E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {35584632-2D73-4767-B2C0-7DB1ABB2812E}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {35584632-2D73-4767-B2C0-7DB1ABB2812E}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {35584632-2D73-4767-B2C0-7DB1ABB2812E}.Release|Any CPU.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | GlobalSection(NestedProjects) = preSolution 48 | {00E474D3-ACA6-4210-8E9B-AD94ED920647} = {000D6020-F5D7-4CC9-A539-36914452C63B} 49 | EndGlobalSection 50 | GlobalSection(ExtensibilityGlobals) = postSolution 51 | SolutionGuid = {82099E13-9449-4737-8EAE-1188CE33BC09} 52 | EndGlobalSection 53 | EndGlobal 54 | -------------------------------------------------------------------------------- /CSharpRepl.sln.licenseheader: -------------------------------------------------------------------------------- 1 | extensions: .cs 2 | // This Source Code Form is subject to the terms of the Mozilla Public 3 | // License, v. 2.0. If a copy of the MPL was not distributed with this 4 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 5 | -------------------------------------------------------------------------------- /CSharpRepl/CSharpRepl.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 0.6.7 5 | Exe 6 | net9.0 7 | LatestMajor 8 | latest 9 | enable 10 | 11 | A dotnet global tool for a command line C# REPL with syntax highlighting. Explore the language, libraries and NuGet packages interactively. 12 | README.md 13 | https://github.com/waf/CSharpRepl 14 | git 15 | Will Fuqua 16 | 17 | 18 | true 19 | csharprepl 20 | repl console cli csharp tool 21 | ./nupkg 22 | https://github.com/waf/CSharpRepl 23 | https://github.com/waf/CSharpRepl/blob/main/CHANGELOG.md 24 | MPL-2.0 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | PreserveNewest 49 | 50 | 51 | PreserveNewest 52 | 53 | 54 | PreserveNewest 55 | 56 | 57 | PreserveNewest 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /CSharpRepl/ConfigurationFile.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.CommandLine; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Security; 11 | using static System.Environment; 12 | 13 | namespace CSharpRepl; 14 | 15 | /// 16 | /// The CSharpRepl configuration file is an RSP (response) file. This is the same filetype that csi and msbuild use. 17 | /// System.CommandLine supports it natively, so this file is just some helper functions. 18 | /// 19 | internal static class ConfigurationFile 20 | { 21 | public static void CreateDefaultConfigurationFile(string configFilePath, RootCommand commandLine, Option[] ignoreCommands) 22 | { 23 | try 24 | { 25 | var availableOptions = commandLine.Options 26 | .Where(option => !ignoreCommands.Contains(option)) 27 | .Select(option => $"{NewLine}# {option.Description}{NewLine}# --{option.Name} <{option.ValueType.Name.ToLower()}>"); 28 | File.WriteAllText( 29 | configFilePath, 30 | "# Add csharprepl command line options to this file to configure csharprepl." + NewLine + 31 | "# You may uncomment an option below by removing the leading '#' character." + NewLine + 32 | string.Join(NewLine, availableOptions) 33 | ); 34 | } 35 | catch (Exception ex) when (ex is IOException or SecurityException or UnauthorizedAccessException or DirectoryNotFoundException) 36 | { 37 | // If creating the default config file fails, don't consider that fatal, just warn and move on. 38 | Console.WriteLine("Warning, could not create default configuration file at path: " + configFilePath); 39 | } 40 | } 41 | 42 | public static void LaunchEditor(string configFilePath) 43 | { 44 | // prefer whatever is in the EDITOR environment variable, then fall back to vscode, then notepad/vim depending on OS. 45 | try 46 | { 47 | var editorName = GetEnvironmentVariable("EDITOR"); 48 | if (editorName is null) 49 | { 50 | var vsCodeLocationProcess = Process.Start(new ProcessStartInfo 51 | { 52 | FileName = OperatingSystem.IsWindows() ? "where" : "which", 53 | Arguments = "code", 54 | UseShellExecute = false, 55 | CreateNoWindow = true, 56 | WindowStyle = ProcessWindowStyle.Hidden, 57 | RedirectStandardOutput = true, 58 | }); 59 | vsCodeLocationProcess?.WaitForExit(); 60 | editorName = vsCodeLocationProcess?.ExitCode == 0 ? "code" : null; 61 | } 62 | editorName ??= OperatingSystem.IsWindows() ? "notepad.exe" : "vim"; 63 | Process 64 | .Start(new ProcessStartInfo 65 | { 66 | FileName = editorName, 67 | Arguments = configFilePath, 68 | UseShellExecute = true, 69 | CreateNoWindow = true, 70 | WindowStyle = editorName == "code" ? ProcessWindowStyle.Hidden : ProcessWindowStyle.Normal 71 | }) 72 | ?.WaitForExit(); 73 | } 74 | catch (Exception ex) 75 | { 76 | Console.Error.WriteLine("Could not launch editor for file: " + configFilePath); 77 | Console.Error.WriteLine(ex.Message); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CSharpRepl/Logging/NullLogger.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using CSharpRepl.Services.Logging; 8 | 9 | namespace CSharpRepl.Logging; 10 | 11 | /// 12 | /// NullLogger is used by default. is used when the --trace flag is provided. 13 | /// 14 | internal sealed class NullLogger : ITraceLogger 15 | { 16 | public void Log(string message) { /* null logger does not log */ } 17 | public void Log(Func message) { /* null logger does not log */ } 18 | public void LogPaths(string message, Func> paths) { /* null logger does not log */ } 19 | } 20 | -------------------------------------------------------------------------------- /CSharpRepl/Logging/TraceLogger.cs: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at https://mozilla.org/MPL/2.0/. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using CSharpRepl.Services.Logging; 11 | 12 | namespace CSharpRepl.Logging; 13 | 14 | /// 15 | /// TraceLogger is used when the --trace flag is provided, and logs debugging details to a file. 16 | /// 17 | internal sealed class TraceLogger : ITraceLogger 18 | { 19 | private readonly string path; 20 | 21 | private TraceLogger(string path) => this.path = path; 22 | 23 | public void Log(string message) => File.AppendAllText(path, $"{DateTime.UtcNow:s} - {message}{Environment.NewLine}"); 24 | 25 | public void Log(Func message) => Log(message()); 26 | 27 | public void LogPaths(string message, Func> paths) => Log(message + ": " + GroupPathsByPrefixForLogging(paths())); 28 | 29 | public static ITraceLogger Create(string path) 30 | { 31 | var tracePath = Path.GetFullPath(path); 32 | var logger = new TraceLogger(tracePath); 33 | 34 | // let the user know where the trace is being logged to, by writing to the REPL. 35 | Console.Write(Environment.NewLine + "Writing trace log to "); 36 | Console.ForegroundColor = ConsoleColor.Green; 37 | Console.WriteLine(tracePath + Environment.NewLine); 38 | Console.ResetColor(); 39 | 40 | AppDomain.CurrentDomain.UnhandledException += 41 | (_, evt) => logger.Log("Unhandled Exception: " + evt.ExceptionObject.ToString()); 42 | TaskScheduler.UnobservedTaskException += 43 | (_, evt) => logger.Log("Unoberved Task Exception: " + evt.Exception.ToString()); 44 | 45 | logger.Log("Trace session starting"); 46 | 47 | return logger; 48 | } 49 | 50 | private static string GroupPathsByPrefixForLogging(IEnumerable paths) => 51 | string.Join( 52 | ", ", 53 | paths 54 | .GroupBy(Path.GetDirectoryName) 55 | .Select(group => $@"""{group.Key}"": [{string.Join(", ", group.Select(path => $@"""{Path.GetFileName(path)}"""))}]") 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /CSharpRepl/PipedInputEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using CSharpRepl.Services; 5 | using CSharpRepl.Services.Roslyn; 6 | using CSharpRepl.Services.Roslyn.Scripting; 7 | 8 | namespace CSharpRepl; 9 | 10 | /// 11 | /// CSharpRepl is predominantly an interactive repl, but also supports input being piped to the executable. 12 | /// This class handles the piped non-interactive input mode. 13 | /// 14 | internal sealed class PipedInputEvaluator 15 | { 16 | private readonly IConsoleEx console; 17 | private readonly RoslynServices roslyn; 18 | 19 | public PipedInputEvaluator(IConsoleEx console, RoslynServices roslyn) 20 | { 21 | this.console = console; 22 | this.roslyn = roslyn; 23 | } 24 | 25 | /// 26 | /// When we're receiving pipe input, evaluate the input as it streams in. 27 | /// 28 | /// exit / error code 29 | public async Task EvaluateStreamingPipeInputAsync() 30 | { 31 | // input could be piped forever, so don't read all the input and then evaluate it in one go. 32 | // instead, read the input line by line until we have a completed statement, then evaluate that. 33 | 34 | var statement = new StringBuilder(); 35 | string? inputLine; 36 | while ((inputLine = console.ReadLine()) is not null) 37 | { 38 | // batch input into a complete statement 39 | statement.AppendLine(inputLine); 40 | string input = statement.ToString(); 41 | if (!await roslyn.IsTextCompleteStatementAsync(input)) 42 | { 43 | continue; 44 | } 45 | statement.Clear(); 46 | 47 | // evaluate complete statement. 48 | var result = await roslyn.EvaluateAsync(input); 49 | if (result is not EvaluationResult.Success) 50 | { 51 | return ErrorCode(result); 52 | } 53 | } 54 | 55 | return ExitCodes.Success; 56 | } 57 | 58 | /// 59 | /// Reads all input from stdin in one go, and evaluates it and returns. 60 | /// Could block forever if input never ends. 61 | /// 62 | /// exit / error code 63 | public async Task EvaluateCollectedPipeInputAsync() 64 | { 65 | var input = new StringBuilder(); 66 | string? line; 67 | while ((line = console.ReadLine()) is not null) 68 | { 69 | input.AppendLine(line); 70 | } 71 | 72 | var result = await roslyn.EvaluateAsync(input.ToString()); 73 | return result is EvaluationResult.Success 74 | ? ExitCodes.Success 75 | : ErrorCode(result); 76 | } 77 | 78 | private int ErrorCode(EvaluationResult result) 79 | { 80 | switch (result) 81 | { 82 | case EvaluationResult.Error err: 83 | console.WriteErrorLine(err.Exception.Message); 84 | return err.Exception.HResult; 85 | case EvaluationResult.Cancelled: 86 | return ExitCodes.ErrorCancelled; 87 | default: 88 | throw new InvalidOperationException("Unhandled EvaluationResult type"); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /CSharpRepl/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CSharpRepl": { 4 | "commandName": "Project", 5 | "workingDirectory": "C:\\" 6 | }, 7 | "CSharpRepl Custom Configuration": { 8 | "commandName": "Project", 9 | "commandLineArgs": "--newLineKeys Enter Shift+Enter --submitPromptKeys Ctrl+Enter --submitPromptDetailedKeys Ctrl+Shift+Enter Ctrl+Alt+Enter --prompt \"[#B9FF51]>[/] \"" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /CSharpRepl/themes/dracula.json: -------------------------------------------------------------------------------- 1 | { 2 | "selectedCompletionItemBackground": "#3C3C3C", 3 | "completionBoxBorderColor": "#909090", 4 | "completionItemDescriptionPaneBackground": "#282828", 5 | "selectedTextBackground": "#143D66", 6 | "syntaxHighlightingColors": [ 7 | { 8 | "name": "class name", 9 | "foreground": "#8BE9FD" 10 | }, 11 | { 12 | "name": "struct name", 13 | "foreground": "#8BE9FD" 14 | }, 15 | { 16 | "name": "delegate name", 17 | "foreground": "#8BE9FD" 18 | }, 19 | { 20 | "name": "interface name", 21 | "foreground": "#8BE9FD" 22 | }, 23 | { 24 | "name": "module name", 25 | "foreground": "#8BE9FD" 26 | }, 27 | { 28 | "name": "record class name", 29 | "foreground": "#8BE9FD" 30 | }, 31 | { 32 | "name": "record struct name", 33 | "foreground": "#8BE9FD" 34 | }, 35 | { 36 | "name": "enum name", 37 | "foreground": "#50FA7B" 38 | }, 39 | { 40 | "name": "constant name", 41 | "foreground": "#F8F8F2" 42 | }, 43 | { 44 | "name": "enum member name", 45 | "foreground": "#F8F8F2" 46 | }, 47 | { 48 | "name": "event name", 49 | "foreground": "#F8F8F2" 50 | }, 51 | { 52 | "name": "extension method name", 53 | "foreground": "#F8F8F2" 54 | }, 55 | { 56 | "name": "text", 57 | "foreground": "#F8F8F2" 58 | }, 59 | { 60 | "name": "identifier", 61 | "foreground": "#F8F8F2" 62 | }, 63 | { 64 | "name": "label name", 65 | "foreground": "#F8F8F2" 66 | }, 67 | { 68 | "name": "local name", 69 | "foreground": "#F8F8F2" 70 | }, 71 | { 72 | "name": "method name", 73 | "foreground": "#F8F8F2" 74 | }, 75 | { 76 | "name": "property name", 77 | "foreground": "#F8F8F2" 78 | }, 79 | { 80 | "name": "namespace name", 81 | "foreground": "#F8F8F2" 82 | }, 83 | { 84 | "name": "parameter name", 85 | "foreground": "#F8F8F2" 86 | }, 87 | { 88 | "name": "number", 89 | "foreground": "#BD93F9" 90 | }, 91 | { 92 | "name": "keyword - control", 93 | "foreground": "#FF79C6" 94 | }, 95 | { 96 | "name": "keyword", 97 | "foreground": "#FF79C6" 98 | }, 99 | { 100 | "name": "operator", 101 | "foreground": "#FF79C6" 102 | }, 103 | { 104 | "name": "operator - overloaded", 105 | "foreground": "#FF79C6" 106 | }, 107 | { 108 | "name": "preprocessor keyword", 109 | "foreground": "#FF79C6" 110 | }, 111 | { 112 | "name": "string - escape character", 113 | "foreground": "#FF79C6" 114 | }, 115 | { 116 | "name": "string - verbatim", 117 | "foreground": "#F1FA8C" 118 | }, 119 | { 120 | "name": "string", 121 | "foreground": "#F1FA8C" 122 | }, 123 | { 124 | "name": "type parameter name", 125 | "foreground": "#FFB86C" 126 | }, 127 | { 128 | "name": "comment", 129 | "foreground": "#6272A4" 130 | }, 131 | { 132 | "name": "xml doc comment - attribute quotes", 133 | "foreground": "#50FA7B" 134 | }, 135 | { 136 | "name": "xml doc comment - attribute value", 137 | "foreground": "#50FA7B" 138 | }, 139 | { 140 | "name": "xml doc comment - attribute name", 141 | "foreground": "#6272A4" 142 | }, 143 | { 144 | "name": "xml doc comment - cdata section", 145 | "foreground": "#6272A4" 146 | }, 147 | { 148 | "name": "xml doc comment - comment", 149 | "foreground": "#6272A4" 150 | }, 151 | { 152 | "name": "xml doc comment - delimiter", 153 | "foreground": "#6272A4" 154 | }, 155 | { 156 | "name": "xml doc comment - entity reference", 157 | "foreground": "#6272A4" 158 | }, 159 | { 160 | "name": "xml doc comment - name", 161 | "foreground": "#6272A4" 162 | }, 163 | { 164 | "name": "xml doc comment - processing instruction", 165 | "foreground": "#6272A4" 166 | }, 167 | { 168 | "name": "xml doc comment - text", 169 | "foreground": "#6272A4" 170 | } 171 | ] 172 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /publish-release.ps1: -------------------------------------------------------------------------------- 1 | $localChanges = git status --short 2 | if ( $null -ne $localChanges ) { 3 | Write-Output "Uncommitted changes detected, aborting release." 4 | Exit 1 5 | } 6 | 7 | git fetch origin 8 | $remoteChanges = git log HEAD..origin/main --oneline 9 | if ( $null -ne $remoteChanges ) { 10 | Write-Output "The main branch is out of date, aborting release." 11 | Exit 2 12 | } 13 | 14 | $csproj = [xml](Get-Content ./CSharpRepl/CSharpRepl.csproj) 15 | $version = $csproj.Project.PropertyGroup.Version 16 | 17 | Write-Output "Reminder: Did you update the CHANGELOG.md?" 18 | Write-Output "Press Enter to create tag ""v$version"" and publish to nuget.org" 19 | Read-Host 20 | 21 | git tag "v$version" 22 | git push origin "v$version" 23 | --------------------------------------------------------------------------------