diff --git a/.editorconfig b/.editorconfig index 6e9e7984a..bc07e9f86 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,4 +9,7 @@ fsharp_blank_lines_around_nested_multiline_expressions=false fsharp_keep_max_number_of_blank_lines=1 [tests/*.fsx] -fsharp_multiline_bracket_style = aligned \ No newline at end of file +fsharp_multiline_bracket_style = aligned + +[mcp/**/*.fsx] +fsharp_multiline_bracket_style = stroustrup \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..5c49eabd6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,576 @@ +# AGENTS.md - AI Agent Guide for Fantomas + +This guide is specifically designed for AI agents working on the Fantomas F# code formatter project. It provides essential information about project structure, development workflow, and best practices. + +## Table of Contents + +1. [Project Overview](#project-overview) +2. [Project Structure](#project-structure) +3. [Development Workflow](#development-workflow) +4. [Core Architecture](#core-architecture) +5. [Testing Guidelines](#testing-guidelines) +6. [Common Tasks](#common-tasks) +7. [MCP Tools](#mcp-tools) +8. [Configuration](#configuration) +9. [Error Handling](#error-handling) +10. [Code Style](#code-style) +11. [Troubleshooting](#troubleshooting) +12. [Resources](#resources) + +## Project Overview + +Fantomas is an F# code formatter that transforms F# source code into a standardized format. The project follows a two-phase approach: + +1. **Transform**: Parse F# source code into a custom tree model (Oak) +2. **Traverse**: Walk the tree to generate formatted code + +### Key Principles + +- **Valid Code Only**: Fantomas requires valid F# source code to format +- **Test-Driven Development**: Always write tests before implementing fixes +- **Minimal Changes**: Make the smallest possible changes to achieve the goal +- **Consistency**: Follow existing code patterns and conventions + +### Quick Start Commands + +```bash +# Most common commands for LLMs +dotnet build # Quick build check (use during development) +dotnet test --filter "test-name" # Run specific test +dotnet fsi build.fsx -p FormatChanged # Format changes +dotnet fsi build.fsx # Full build and test (use for final verification) +dotnet fsi build.fsx -p FormatAll # Format all files +dotnet fsi build.fsx -- -p Analyze # Run code analyzers +``` + +## Project Structure + +``` +fantomas/ +├── src/ +│ ├── Fantomas.Core/ # Core formatting engine +│ │ ├── CodePrinter.fs # Main code generation logic +│ │ ├── SyntaxOak.fs # Custom tree model (also includes types of Trivia) +│ │ └── Context.fs # Formatting context and events (includes lots of helpers) +│ ├── Fantomas.Core.Tests/ # Unit tests +│ │ ├── ModuleTests.fs # Module-related tests +│ │ └── TestHelpers.fs # Test utilities +│ ├── Fantomas.FCS/ # F# Compiler Service integration +│ └── Fantomas/ # CLI interface +├── docs/ # Documentation +├── build.fsx # Build script +└── global.json # .NET SDK version +``` + +### Key Files for AI Agents + +### `src/Fantomas.Core/CodePrinter.fs` + +- Main formatting logic +- Constructs pipeline for `Context` instance to collect `WriterEvents` based on `Oak` model +- `genNode` is common helper for `Trivia` of `Node` to be processed +- **Critical**: Changes to `genNode` impact the entire file - scope logic closer to where problems reside instead +- Careful with edits to auxiliary functions, changing these can be very impactful, consider local edits instead + +### `src/Fantomas.Core/SyntaxOak.fs` + +- Custom tree model definitions +- `Node` can hold `Trivia` where nodes from `Fantomas.FCS.Syntax.ParsedInput` cannot +- `TriviaContent` is union types for the trivia types (comments, newlines, directives, cursor) + +### `src/Fantomas.Core/Trivia.fs` + +- Trivia assigment +- Different algoritm based `TriviaContent` +- Assigment is best effort approach, hard to perfect + +### `src/Fantomas.Core/Context.fs` + +- `WriterEvents` models the formatted result code without committing yet. +- Often a simple code formatting path is tried, to only be reverted when the code does not fit a given threshold + +### `src/Fantomas.Core.Tests/*Tests.fs` + +- Test files have a fixed format +- Input and output in test should be valid F# code +- Try and change as little as possible config values + +## Development Workflow + +### 1. Setup + +```bash +# Clone and setup +git clone +cd fantomas +dotnet tool restore +dotnet restore + +# Build and test +dotnet fsi build.fsx +``` + +### 2. Making Changes + +1. **Write Test First**: Create a failing test that demonstrates the issue +2. **Implement Fix**: Make minimal changes to fix the issue +3. **Verify**: Run tests to ensure fix works +4. **Format**: Format your changes using the build script + +### 3. Testing + +```bash +# Quick build check during development +dotnet build + +# Run specific test +dotnet test src/Fantomas.Core.Tests/ --filter "test-name" + +# Run tests in release mode (recommended to avoid stack overflows on Mac) +dotnet test src/Fantomas.Core.Tests/ --filter "test-name" -c Release + +# Format changed files +dotnet fsi build.fsx -p FormatChanged + +# Full build and test (use for final verification) +dotnet fsi build.fsx +``` + +### 4. Build Commands + +```bash +# Full build with tests +dotnet fsi build.fsx + +# Format all files +dotnet fsi build.fsx -p FormatAll + +# Format only changed files +dotnet fsi build.fsx -p FormatChanged + +# Setup git hooks +dotnet fsi build.fsx -p EnsureRepoConfig + +# Run code analyzers +dotnet fsi build.fsx -- -p Analyze +``` + +## Core Architecture + +### Two-Phase Processing + +#### Phase 1: Transform to Oak +1. **Parse**: Use F# compiler to parse source into untyped AST +2. **Transform**: Map AST to custom Oak tree model +3. **Collect Trivia**: Add comments, directives, and blank lines + +#### Phase 2: Traverse to Code +1. **Generate Events**: Walk Oak tree to create WriterEvents +2. **Apply Context**: Use Context to manage indentation and formatting +3. **Output**: Convert events to formatted source code + +### Key Components + +#### Oak Tree Model +- **NodeBase**: Base class for all nodes with trivia support +- **SingleTextNode**: Represents text tokens +- **IdentListNode**: Represents identifiers with dots +- **ModuleOrNamespaceNode**: Represents module/namespace declarations + +#### Context System +- **WriterEvents**: Capture formatting actions (indent, newline, text) +- **WriterModel**: Track current state (column, indentation level) +- **Config**: Formatting configuration options + +#### Indentation System +- **Indentation Events**: `IndentBy`, `UnIndentBy`, `SetIndent`, `RestoreIndent` +- **Column Tracking**: `AtColumn` helps maintain alignment at specific positions +- **Critical Rule**: Indentation only takes effect after `WriteLine`/`WriteLineBecauseOfTrivia` events +- **Common Pattern**: `indent` → `sepNln` → content → `unindent` +- **Helper Functions**: `indentSepNlnUnindent`, `atCurrentColumn`, `atCurrentColumnIndent` + +#### Trivia System +- **ContentBefore/ContentAfter**: Attach comments and directives to nodes +- **Directive**: Conditional compilation directives (#if, #endif) +- **Comment**: Line and block comments +- **Newline**: Blank lines and spacing + +### Common Patterns + +#### Adding New Oak Node Types +1. Define the node type in `SyntaxOak.fs` +2. Add transformation logic in `ASTTransformer.fs` +3. Add formatting logic in `CodePrinter.fs` +4. Update tests in appropriate `*Tests.fs` file + +#### CodePrinter Patterns +- Use `genNode` for common trivia handling +- Use `genTriviaFor` for specific trivia types +- Use `dumpAndContinue` for debugging context state +- Prefer local helper functions over modifying global ones + +#### Trivia Assignment Patterns +- Use `Trivia.enrichTree` to attach trivia to nodes +- Check `hasDirectivesInContentBefore` for conditional compilation +- Use `insertCursor` for cursor position handling + +## Testing Guidelines + +### Test Structure + +```fsharp +[] +let ``descriptive test name, issue-number`` () = + formatSourceString """ + // Input code here + """ config + |> prepend newline + |> should equal """ + // Expected output here + """ +``` + +### Test Naming Convention + +- Start with lowercase letter +- Use descriptive names +- Include issue number for bug fixes: `"fix description, 1234"` +- Use backticks for multi-word names + +### Test Categories + +- We organize the tests in `Fantomas.Core.Tests` according to syntax constructs: + - **ModuleTests.fs**: Module and namespace formatting + - **TypeTests.fs**: Type definition formatting + - **ExpressionTests.fs**: Expression formatting + - **PatternTests.fs**: Pattern matching formatting +- `src/Fantomas.Core.Tests/CodePrinterHelperFunctionsTests.fs` can be insightful to understand the `Context` pipeline setup. + +### Writing Tests + +1. **Reproduce Issue**: Create test that shows the problem +2. **Set Expectations**: Define expected output +3. **Verify Fix**: Ensure test passes after implementation +4. **Add Variations**: Test edge cases and similar scenarios + +## Common Tasks + +### Fixing Formatting Issues + +1. **Identify Problem**: Understand what's wrong with current formatting +2. **Locate Code**: Find relevant code in `CodePrinter.fs` +3. **Write Test**: Create failing test for the issue +4. **Implement Fix**: Modify formatting logic +5. **Verify**: Run tests and check for regressions + +### Adding New Features + +1. **Understand Requirement**: Clarify what needs to be formatted +2. **Find Similar Code**: Look for existing patterns in CodePrinter +3. **Extend Oak Model**: Add new node types if needed +4. **Implement Logic**: Add formatting logic +5. **Add Tests**: Cover all code paths and edge cases + +### Working with Conditional Directives + +- Directives are stored as trivia on nodes +- Use `hasDirectivesInContentBefore` to detect directives +- Apply special formatting when directives are present +- Test with different define combinations + +### Debugging CodePrinter + +- Use `dumpAndContinue` to inspect Context during traversal +- Breakpoints in CodePrinter show function composition, not execution +- Check WriterEvents to understand formatting decisions +- Use MCP tools to test formatting in real-time + +### Understanding Indentation + +Indentation in Fantomas is **deferred** - it only takes effect after newline events: + +```fsharp +// This pattern is very common in CodePrinter: +indent +> sepNln +> content +> unindent + +// Or using the helper: +indentSepNlnUnindent content +``` + +**Key Points:** +- `indent` adds an `IndentBy` event to the context +- The actual indentation only applies when `WriteLine`/`WriteLineBecauseOfTrivia` events are processed +- `WriterModel.update` in `Context.fs` handles this by setting `Indent = max m.Indent m.AtColumn` on newlines +- Always pair `indent` with `unindent` to avoid indentation drift +- Use `atCurrentColumn` and `atCurrentColumnIndent` for fixed column positioning + +**Example from CodePrinterHelperFunctionsTests.fs:** +```fsharp +let g = !-"first line" +> indent +> sepNln +> !-"second line" +> unindent +// Result: "first line\n second line" +``` + +### Understanding genNode and Trivia Processing + +The `genNode` function is crucial for understanding how indentation interacts with trivia: + +```fsharp +let genNode<'n when 'n :> Node> (n: 'n) (f: Context -> Context) = + enterNode n +> recordCursorNode f n +> leaveNode n + +let enterNode<'n when 'n :> Node> (n: 'n) = + col sepNone n.ContentBefore (genTrivia n) +``` + +**Critical Understanding:** +- `genNode` processes `ContentBefore` trivia first, then runs the function, then `ContentAfter` trivia +- `genTrivia` handles directives and comments, often emitting `sepNlnForTrivia` events +- **Trivia processing can override indentation**: When `genTrivia` processes directives, it may emit newline events that apply indentation before your intended content +- This is why `indentSepNlnUnindent` patterns can fail when nodes have trivia - the trivia emits newlines that consume the indentation + +**Common Issue Pattern:** +```fsharp +// This fails when node has ContentBefore trivia: +indentSepNlnUnindent (genIdentListNode node) +// Because genIdentListNode calls genNode, which processes trivia first +// The trivia emits newlines that apply indentation before the module name +``` + +**Solution Approaches:** +1. **Bypass genNode**: Write content directly without trivia processing +2. **Handle trivia separately**: Process trivia before applying indentation +3. **Use different indentation strategy**: Apply indentation at a different level in the tree + +## MCP Tools + +### Fantomas Format Code Tool + +The MCP Fantomas tool provides real-time formatting testing and debugging capabilities. It's essential for understanding how code transformations work. + +#### Usage + +```bash +# Tool configuration (in ~/.cursor/mcp.json) +"Fantomas": { + "type": "stdio", + "command": "dotnet", + "args": ["fsi", "/path/to/fantomas/mcp/server.fsx"] +} +``` + +#### Expected Usage + +1. **Input Requirements**: The tool expects valid F# code as input + - Invalid F# code will result in parse errors and limited debugging information + - Use the tool with problematic but syntactically correct F# code + +2. **Tool Behavior**: + - Automatically builds the local Fantomas codebase before formatting + - Reports detailed events during the formatting process + - Shows transformation from F# source → Untyped AST → Syntax Oak → WriterEvents + - Validates that formatted output is still valid F# code + +3. **Debugging Workflow**: + ```fsharp + // Start with problematic code + let problematicCode = """ + module Test + let x=1+2 + """ + + // Use MCP tool to see transformation events + // Analyze WriterEvents to understand formatting decisions + // Identify where formatting logic needs adjustment + ``` + +4. **Integration with Development**: + - Use before writing tests to understand expected behavior + - Use to debug existing formatting issues + - Use to verify fixes work as expected + +## Configuration + +### Finding Configuration Options + +Configuration is primarily handled through `FormatConfig` in the codebase: + +```fsharp +// Common configuration options +type FormatConfig = { + IndentSize: int + MaxLineLength: int + // ... other options +} +``` + +### Configuration Best Practices + +1. **Minimal Changes**: Try to change as few config values as possible +2. **Test Impact**: Always test configuration changes with existing tests +3. **Documentation**: Document why specific config values are needed +4. **Default Behavior**: Prefer using default configuration when possible + +### Common Configuration Patterns + +```fsharp +// Use default config for most tests +let config = FormatConfig.Default + +// Only modify specific settings when necessary +let customConfig = + { FormatConfig.Default with + IndentSize = 2 + MaxLineLength = 120 } +``` + +## Error Handling + +### Parse Errors + +Fantomas requires valid F# code to format. Parse errors are handled in `CodeFormatterImpl.fs`: + +```fsharp +// Parse errors raise ParseException +if not errors.IsEmpty then + raise (ParseException baseDiagnostics) +``` + +#### Common Parse Error Scenarios + +1. **Syntax Errors**: Invalid F# syntax will cause parse failures +2. **Missing Dependencies**: Unresolved type references +3. **Conditional Compilation**: Issues with `#if` directives + +#### Handling Parse Errors + +- **For Testing**: Ensure test input is valid F# code +- **For Development**: Fix syntax errors before testing formatting +- **For MCP Tool**: Use valid F# code to get meaningful debugging output + +### Format Errors + +#### "The formatted result is not valid F# code" +- Check if formatting logic produces syntactically correct output +- Verify indentation and spacing are correct +- Test with F# compiler to ensure validity + +#### WriterEvent Errors +- Check Context state during formatting +- Use `dumpAndContinue` to inspect intermediate states +- Verify WriterEvents produce valid code sequences + +## Code Style + +### F# Conventions Used in Fantomas + +1. **Naming**: + - Use camelCase for functions and variables + - Use PascalCase for types and modules + - Use descriptive names that explain intent + +2. **Formatting**: + - Follow Fantomas' own formatting rules + - Use consistent indentation (4 spaces) + - Prefer composition over complex expressions + +3. **Patterns**: + - Use discriminated unions for tree nodes + - Use active patterns for AST matching + - Prefer immutable data structures + +4. **Comments**: + - Use `//` for single-line comments + - Use `(* *)` for multi-line comments + - Document complex algorithms and decisions + +### File Organization + +- **One type per file** when possible +- **Logical grouping** of related functions +- **Clear separation** between public and internal APIs + +## Troubleshooting + +### Common Issues + +#### "The formatted result is not valid F# code" +- Check if the fix produces syntactically correct F# code +- Verify indentation and spacing +- Test with F# compiler to ensure validity + +#### Tests Failing +- Ensure test expectations match actual output +- Check for whitespace differences +- Verify test is using correct configuration + +#### Build Errors +- Run `dotnet clean` and rebuild +- Check for syntax errors in your changes +- Ensure all dependencies are restored + +#### Stack Overflow on Mac +- Stack overflows can occur in debug mode on macOS +- Use `-c Release` flag when running tests: `dotnet test -c Release` +- This is a known issue with F# compilation in debug mode on macOS + +### Debugging Tips + +1. **Use MCP Tools**: Test formatting with `mcp_Fantomas_format_code` +2. **Check Events**: Examine WriterEvents to understand formatting +3. **Compare Outputs**: Use diff tools to compare expected vs actual +4. **Isolate Changes**: Make minimal changes and test incrementally + +## Resources + +### Documentation +- [Getting Started](https://fsprojects.github.io/fantomas/docs/contributors/Getting%20Started.html) +- [Core Architecture](https://fsprojects.github.io/fantomas/docs/contributors/Transforming.html) +- [CodePrinter Guide](https://fsprojects.github.io/fantomas/docs/contributors/Traverse.html) +- [Pull Request Guidelines](https://fsprojects.github.io/fantomas/docs/contributors/Pull%20request%20ground%20rules.html) + +### Key Concepts +- [Conditional Compilation Directives](https://fsprojects.github.io/fantomas/docs/contributors/Conditional%20Compilation%20Directives.html) +- [Trivia Handling](https://fsprojects.github.io/fantomas/docs/contributors/The%20Missing%20Comment.html) +- [Multiple Defines](https://fsprojects.github.io/fantomas/docs/contributors/Multiple%20Times.html) + +### Tools +- **MCP Fantomas**: Real-time formatting testing and debugging +- **Build Script**: `dotnet fsi build.fsx` for all operations +- **Test Framework**: NUnit with FsUnit assertions +- **Git Hooks**: Automatic formatting on commit + +### Best Practices + +1. **Always Test First**: Write failing test before implementing fix +2. **Minimal Changes**: Make smallest possible change to fix issue +3. **Follow Patterns**: Use existing code patterns and conventions +4. **Document Changes**: Add meaningful commit messages and PR descriptions +5. **Verify Validity**: Ensure formatted code is valid F# code +6. **Check Regressions**: Run full test suite to catch unintended changes + +### Example Workflow + +```bash +# 1. Setup +git checkout -b fix-3188 +dotnet build + +# 2. Write test +# Add test to ModuleTests.fs + +# 3. Implement fix +# Modify CodePrinter.fs + +# 4. Test and verify +dotnet test src/Fantomas.Core.Tests/ --filter "Name~3188" +dotnet build + +# 5. Format changes +dotnet fsi build.fsx -p FormatChanged + +# 6. Final verification +dotnet fsi build.fsx +``` + +This guide should help AI agents understand the Fantomas project structure and contribute effectively to the codebase. + diff --git a/Directory.Packages.props b/Directory.Packages.props index d3eb0e1bf..73dce6e45 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,42 +1,39 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fantomas.sln b/fantomas.sln index 455820b02..d3f08fe48 100644 --- a/fantomas.sln +++ b/fantomas.sln @@ -153,9 +153,9 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DEBFC920-A647-4859-890D-B59793D27594} EndGlobalSection - GlobalSection(NestedProjects) = preSolution - EndGlobalSection EndGlobal diff --git a/global.json b/global.json index 6f6aff9de..d3dbe0937 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.400", + "version": "9.0.300", "rollForward": "latestPatch" } } \ No newline at end of file diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 000000000..f8a287194 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,53 @@ + +# MCP + +## Goal + +Give contributors more tools to use AI to come up with fixes to the codebase. +Having a set of specific tools calls could improve the hallucinating and help an LLM avoid a bunch of BS. + +## Tool calls + +I think we will need to have a second fsi script which the mcp server can call. +Because we don't want a hard dependency on the source code. +Maybe the LLM made a change which leads to a broken code base. + +### Parse + +expose the parser as tool call. +Return both untyped F# AST and Oak? + +### Format + +expose the format call. +input should be source code, isFsi, settings (.editorconfig) +Include validation here as well? + +### Rebuild + +Produce a new binary of Fantomas.Core. +Should probably be called in most other tool calls. + +### Trivia + +Analyze which trivia was detected in which nodes. + +### Define solver + +Expose the define solver + +### Style guide + +Fetch both style guides and return in LLM friendly fashion. + +### Test generation + +Guide the LLM to generate tests. + +## Prompts + +We should probably also create a bunch of example prompt on how to ask the LLM to fix certain issues. + +## Test issues + +Try and solve https://github.com/fsprojects/fantomas/issues/3188 diff --git a/mcp/script.fsx b/mcp/script.fsx new file mode 100644 index 000000000..3d0b5ebe9 --- /dev/null +++ b/mcp/script.fsx @@ -0,0 +1,49 @@ +#r "nuget: CliWrap" + +open System.IO +open System.Text +open CliWrap +open CliWrap.Buffered + +let input = "let a = 9" + +let inputStream = new MemoryStream(Encoding.UTF8.GetBytes(input)) + +let repositoryRoot = Path.Combine(__SOURCE_DIRECTORY__, "..") |> Path.GetFullPath + +let fantomasCoreFsproj = + Path.Combine(repositoryRoot, "src/Fantomas.Core/Fantomas.Core.fsproj") + +let buildResult = + Cli + .Wrap("dotnet") + .WithArguments([| "build"; fantomasCoreFsproj; "-v"; "n" |]) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync() + .Task.Result + +if buildResult.ExitCode <> 0 then + let gitStatus = + Cli.Wrap("git").WithArguments("status").ExecuteBufferedAsync().Task.Result + + printfn + $"""Currently the Fantomas code base does not build. + +Build Error: + +{buildResult.StandardOutput}{buildResult.StandardError} + +Git status: + +{gitStatus.StandardOutput} +""" +else + let result = + Cli + .Wrap("dotnet") + .WithArguments([| "fsi"; "/Users/nojaf/Projects/fantomas/mcp/tools/format.fsx" |]) + .WithStandardInputPipe(PipeSource.FromStream(inputStream)) + .ExecuteBufferedAsync() + .Task.Result + + printfn $"Result: {result.StandardOutput}" diff --git a/mcp/server.fsx b/mcp/server.fsx new file mode 100644 index 000000000..c13349ca0 --- /dev/null +++ b/mcp/server.fsx @@ -0,0 +1,134 @@ +#r "nuget: ModelContextProtocol, 0.3.0-preview.4" +#r "nuget: Microsoft.Extensions.Hosting" +#r "nuget: CliWrap" + +// Reference: https://www.slaveoftime.fun/blog/single-fsharp-script-to-write-a-mcp-server---simplified-integration-for-dynamic-script-execution + +open System +open System.IO +open System.Text +open System.ComponentModel +open Microsoft.Extensions.Hosting +open Microsoft.Extensions.Logging +open Microsoft.Extensions.DependencyInjection +open ModelContextProtocol.Server +open CliWrap +open CliWrap.Buffered + +let repositoryRoot = Path.Combine(__SOURCE_DIRECTORY__, "..") |> Path.GetFullPath + +let fantomasCoreFsproj = + Path.Combine(repositoryRoot, "src/Fantomas.Core/Fantomas.Core.fsproj") + +let formatScript = + Path.Combine(repositoryRoot, "mcp", "tools", "format.fsx") |> Path.GetFullPath + +let astScript = + Path.Combine(repositoryRoot, "mcp", "tools", "ast.fsx") |> Path.GetFullPath + +let buildCodebase () = + task { + let! buildResult = + Cli + .Wrap("dotnet") + .WithArguments([| "build"; fantomasCoreFsproj; "-v"; "n" |]) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync() + .Task + + if buildResult.ExitCode = 0 then + return Ok() + else + let! gitStatus = Cli.Wrap("git").WithArguments("status").ExecuteBufferedAsync().Task + + return + Error + $"""Currently the Fantomas code base does not build. + +Build Error: + +{buildResult.StandardOutput}{buildResult.StandardError} + +Git status: + +{gitStatus.StandardOutput} + """ + } + +[] +type FantomasMcpServer() = //as this = + [] + member this.FormatCode(sourceCode: string, isSignature: bool) = + task { + match! buildCodebase () with + | Error(error) -> return error + | Ok() -> + let inputStream = new MemoryStream(Encoding.UTF8.GetBytes(sourceCode)) + + let! result = + Cli + .Wrap("dotnet") + .WithArguments( + [| + yield "fsi" + yield formatScript + if isSignature then + yield "--" + yield "--signature" + |] + ) + .WithStandardInputPipe(PipeSource.FromStream inputStream) + .ExecuteBufferedAsync() + .Task + + return result.StandardOutput + } + + [] + member this.ParseAST(sourceCode: string, isSignature: bool) = + task { + match! buildCodebase () with + | Error(error) -> return error + | Ok() -> + let inputStream = new MemoryStream(Encoding.UTF8.GetBytes(sourceCode)) + + let! result = + Cli + .Wrap("dotnet") + .WithArguments( + [| + yield "fsi" + yield astScript + if isSignature then + yield "--" + yield "--signature" + |] + ) + .WithStandardInputPipe(PipeSource.FromStream inputStream) + .ExecuteBufferedAsync() + .Task + + return result.StandardOutput + } + +let builder = Host.CreateApplicationBuilder(Environment.GetCommandLineArgs()) + +builder.Logging.AddConsole(fun consoleLogOptions -> consoleLogOptions.LogToStandardErrorThreshold <- LogLevel.Trace) +builder.Services.AddMcpServer().WithStdioServerTransport().WithTools() + +builder.Build().RunAsync() |> Async.AwaitTask |> Async.RunSynchronously diff --git a/mcp/tools/ast.fsx b/mcp/tools/ast.fsx new file mode 100644 index 000000000..9d53a49eb --- /dev/null +++ b/mcp/tools/ast.fsx @@ -0,0 +1,81 @@ +#r "nuget: Thoth.Json.System.Text.Json" +#r "../../artifacts/bin/Fantomas.FCS/debug/Fantomas.FCS.dll" +#r "../../artifacts/bin/Fantomas.Core/debug/Fantomas.Core.dll" + +open Fantomas.Core +open Fantomas.FCS.Text +open Thoth.Json.Core +open Thoth.Json.System.Text.Json + +let encodeRange (range: range) = + Encode.object [ + "startLine", Encode.int range.StartLine + "startColumn", Encode.int range.StartColumn + "endLine", Encode.int range.EndLine + "endColumn", Encode.int range.EndColumn + ] + +let encodeDiagnostic (diag: Fantomas.FCS.Parse.FSharpParserDiagnostic) = + Encode.object [ + "severity", Encode.string (string diag.Severity) + "subCategory", Encode.string diag.SubCategory + "range", Encode.losslessOption encodeRange diag.Range + "errorNumber", Encode.lossyOption Encode.int diag.ErrorNumber + "message", Encode.string diag.Message + ] + +let encodeParsedInput (ast: Fantomas.FCS.Syntax.ParsedInput, defines: string list) = + Encode.object [ + "defines", Encode.list (List.map Encode.string defines) + "ast", Encode.string (string ast) + ] + +async { + let input = stdin.ReadToEnd() + + let isSignature = + System.Environment.GetCommandLineArgs() |> Array.contains "--signature" + + let fileType = if isSignature then "signature" else "implementation" + + try + let! untypedTrees = CodeFormatter.ParseAsync(isSignature, input) + + let result = + Encode.object [ + "success", Encode.bool true + "fileType", Encode.string fileType + "untypedTrees", + Encode.array (Array.map encodeParsedInput untypedTrees) + "diagnostics", Encode.list [] // No errors if we got here + ] + |> Encode.toString 2 + + printfn $"Parsed {fileType} file successfully:\n---\n{result}\n---" + with + | ParseException(diagnostics) -> + let result = + Encode.object [ + "success", Encode.bool false + "fileType", Encode.string fileType + "error", Encode.string $"Fantomas.FCS was unable to parse {fileType} input" + "input", Encode.string input + "diagnostics", Encode.list (List.map encodeDiagnostic diagnostics) + ] + |> Encode.toString 2 + + printfn $"Parse exception raised:\n---\n{result}\n---" + | ex -> + let result = + Encode.object [ + "success", Encode.bool false + "fileType", Encode.string fileType + "error", Encode.string $"Unexpected exception during parsing of {fileType} input" + "exception", Encode.string (ex.ToString()) + ] + |> Encode.toString 2 + + printfn $"Unexpected exception:\n---\n{result}\n---" +} +|> Async.RunSynchronously + diff --git a/mcp/tools/format.fsx b/mcp/tools/format.fsx new file mode 100644 index 000000000..8f77715c7 --- /dev/null +++ b/mcp/tools/format.fsx @@ -0,0 +1,260 @@ +#r "nuget: CliWrap" +#r "nuget: Thoth.Json.System.Text.Json" +#r "../../artifacts/bin/Fantomas.FCS/debug/Fantomas.FCS.dll" +#r "../../artifacts/bin/Fantomas.Core/debug/Fantomas.Core.dll" + +open Fantomas.Core + +// let st = Fantomas.Core.CodeFormatterImpl.getSourceText "let a = 0" +// printfn "%A" (st.GetType()) +// +// open Fantomas.Core +// open Thoth.Json.Core +// open Thoth.Json.System.Text.Json +// +// type FormatEvents<'state> = Result +// +// let encodeRange (range: Fantomas.FCS.Text.Range) = +// Encode.object [ +// "startLine", Encode.int range.StartLine +// "startColumn", Encode.int range.StartColumn +// "endLine", Encode.int range.EndLine +// "endColumn", Encode.int range.EndColumn +// ] +// +// type InitialState = { Input: string; IsSignature: bool } +// +// let getSourceText input isSignatureFile : FormatEvents = +// try +// let _ = CodeFormatterImpl.getSourceText input +// +// Ok( +// [ +// Encode.object [ +// "name", Encode.string "Created SourceText" +// "description", Encode.string "Successfully created internal ISourceText value for input" +// ] +// ], +// { +// Input = input +// IsSignature = isSignatureFile +// } +// ) +// +// with ex -> +// Error [ +// Encode.object [ +// "name", Encode.string "Failed to create ISourceText" +// "description", Encode.string $"Could not construct FCS.Text.ISourceText for \"{input}\"" +// "exception", Encode.string (ex.ToString()) +// ] +// ] +// +// [] +// type StateWithUntypedTree = { +// Input: string +// IsSignature: bool +// UntypedTrees: (Fantomas.FCS.Syntax.ParsedInput * string list) array +// } +// +// let parseUntypedTree (events, state: InitialState) = +// async { +// let type_ = if state.IsSignature then "signature" else "implementation" +// +// try +// let! untypedTrees = CodeFormatter.ParseAsync(state.IsSignature, state.Input) +// +// let nextEvents = +// Encode.object [ +// "name", Encode.string "Parsed input to Untyped AST" +// "description", Encode.string "Source input was able to be parsed as untyped AST" +// "untypedTrees", +// Encode.array ( +// untypedTrees +// |> Array.map (fun (ast, defines) -> +// Encode.object [ +// "defines", Encode.list (List.map Encode.string defines) +// "ast", Encode.string (string ast) +// ]) +// ) +// ] +// :: events +// +// return +// Ok( +// nextEvents, +// { +// Input = state.Input +// IsSignature = state.IsSignature +// UntypedTrees = untypedTrees +// } +// ) +// with +// | ParseException(diagnostics) -> +// let nextEvents = +// Encode.object [ +// "name", Encode.string "Parse exception raised during parsing of input to Untyped AST failed" +// "description", Encode.string $"Fantomas.FCS was unable to parse {type_} input" +// "input", Encode.string state.Input +// "diagnostics", +// diagnostics +// |> List.map (fun diag -> +// Encode.object [ +// "severity", Encode.string (string diag.Severity) +// "subCategory", Encode.string diag.SubCategory +// "range", Encode.losslessOption encodeRange diag.Range +// ]) +// |> Encode.list +// ] +// :: events +// +// return Error(nextEvents) +// | ex -> +// let nextEvents = +// Encode.object [ +// "name", Encode.string "Unexpected exception during parsing of input to Untyped AST failed" +// "description", Encode.string $"Fantomas.FCS was unable to parse {type_} input" +// "input", Encode.string state.Input +// "error", Encode.string (ex.ToString()) +// ] +// :: events +// +// return Error(nextEvents) +// } +// +// let (>>=) f g = +// async { +// match f with +// | Error e -> return Error e +// | Ok(e, s) -> return! g (e, s) +// } +// +// let format input isSignature : Async = +// async { +// let! events = getSourceText input isSignature >>= parseUntypedTree +// +// let orderedEvents = +// match events with +// | Error e -> e +// | Ok(e, _) -> e +// |> List.rev +// +// let json = Encode.object [ "events", Encode.list orderedEvents ] +// +// return Encode.toString 2 json +// } +// + +module OakPrinter = + open Thoth.Json.Core + open Thoth.Json.System.Text.Json + open Fantomas.FCS.Text + open Fantomas.Core.SyntaxOak + + let encodeRange (m: range) = + Encode.object [ + "startLine", Encode.int m.StartLine + "startColumn", Encode.int m.StartColumn + "endLine", Encode.int m.EndLine + "endColumn", Encode.int m.EndColumn + ] + + let encodeTriviaNode (triviaNode: TriviaNode) : IEncodable = + let contentType, content = + match triviaNode.Content with + | CommentOnSingleLine comment -> "commentOnSingleLine", Some comment + | LineCommentAfterSourceCode comment -> "lineCommentAfterSourceCode", Some comment + | BlockComment(comment, _, _) -> "blockComment", Some comment + | Newline -> "newline", None + | Directive directive -> "directive", Some directive + | Cursor -> "cursor", None + + Encode.object [ + "range", encodeRange triviaNode.Range + "type", Encode.string contentType + "content", Encode.lossyOption Encode.string content + ] + + let rec encodeNode (node: Node) (continuation: IEncodable -> IEncodable) : IEncodable = + let continuations = List.map encodeNode (Array.toList node.Children) + + let text = + match node with + | :? SingleTextNode as stn -> + if stn.Text.Length < 13 then + stn.Text + else + sprintf "%s.." (stn.Text.Substring(0, 10)) + |> Some + | _ -> None + + let finalContinuation (children: IEncodable list) = + Encode.object [ + yield "type", Encode.string (node.GetType().Name) + match text with + | None -> () + | Some text -> yield ("text", Encode.string text) + yield "range", encodeRange node.Range + if node.HasContentBefore then + yield "contentBefore", Encode.seq (Seq.map encodeTriviaNode node.ContentBefore) + if node.Children.Length > 0 then + yield "children", Encode.list children + if node.HasContentAfter then + yield "contentAfter", Encode.seq (Seq.map encodeTriviaNode node.ContentAfter) + ] + |> continuation + + Continuation.sequence continuations finalContinuation + + let encodeOak (oak: Fantomas.Core.SyntaxOak.Oak) = encodeNode oak id |> Encode.toString 2 + +async { + let input = stdin.ReadToEnd() + + let isSignature = + System.Environment.GetCommandLineArgs() |> Array.contains "--signature" + + let mutable formatted = None + + MCPEvents.resetEvents () + + try + let! output = CodeFormatter.FormatDocumentAsync(isSignature, input) + formatted <- Some(output.Code) + with + | ParseException(diags) -> MCPEvents.addEvent (MCPEvents.EventKind.ParseFailed(diags)) + | ex -> MCPEvents.addEvent (MCPEvents.EventKind.UnExpectedExceptionHappened(ex)) + + let! isValid = + match formatted with + | None -> async { return false } + | Some(formattedCode) -> CodeFormatter.IsValidFSharpCodeAsync(isSignature, formattedCode) + + let events = + MCPEvents.capturedEvents + |> Seq.map (fun event -> + match event.Kind with + | MCPEvents.EventKind.CreatedOakRaw oak -> + string ( + { + event with + Kind = MCPEvents.EventKind.CreatedOak(OakPrinter.encodeOak oak) + } + ) + | _ -> string event) + |> String.concat "\n\n" + + let formatted = formatted |> Option.defaultValue "" + + printfn + $"Events:\n---\n{events}\n---\n\nResult is valid F# code: {isValid}\n---\n\nFormatted code:\n---\n{formatted}---" +} +|> Async.RunSynchronously + + + + +// TODO: don't just dump a response, we should create some event sourced response. +// Numerous things happen here; code builds, input was parsed, trivia detected code formatted, result validated, result trivia detected, etc. +// We need to return this as JSON or some other structured format. +// See https://t3.chat/chat/76da167e-f594-4b3a-9f11-3facb9c11b5e diff --git a/src/Fantomas.Benchmarks/packages.lock.json b/src/Fantomas.Benchmarks/packages.lock.json index bad220a5b..ce30040db 100644 --- a/src/Fantomas.Benchmarks/packages.lock.json +++ b/src/Fantomas.Benchmarks/packages.lock.json @@ -22,9 +22,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "G-Research.FSharp.Analyzers": { "type": "Direct", @@ -266,14 +266,14 @@ "fantomas.core": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "Fantomas.FCS": "[1.0.0, )" } }, "fantomas.fcs": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "System.Collections.Immutable": "[8.0.0, )", "System.Diagnostics.DiagnosticSource": "[8.0.1, )", "System.Memory": "[4.6.0, )", diff --git a/src/Fantomas.Client.Tests/packages.lock.json b/src/Fantomas.Client.Tests/packages.lock.json index 962b7621b..ad23493d5 100644 --- a/src/Fantomas.Client.Tests/packages.lock.json +++ b/src/Fantomas.Client.Tests/packages.lock.json @@ -10,9 +10,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "G-Research.FSharp.Analyzers": { "type": "Direct", @@ -131,7 +131,7 @@ "fantomas.client": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "SemanticVersioning": "[2.0.2, )", "StreamJsonRpc": "[2.20.20, )" } diff --git a/src/Fantomas.Client/packages.lock.json b/src/Fantomas.Client/packages.lock.json index 8f534e435..005444eb2 100644 --- a/src/Fantomas.Client/packages.lock.json +++ b/src/Fantomas.Client/packages.lock.json @@ -16,9 +16,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "G-Research.FSharp.Analyzers": { "type": "Direct", diff --git a/src/Fantomas.Core.Tests/AppTests.fs b/src/Fantomas.Core.Tests/AppTests.fs index 48889da97..edd81bfac 100644 --- a/src/Fantomas.Core.Tests/AppTests.fs +++ b/src/Fantomas.Core.Tests/AppTests.fs @@ -1178,3 +1178,115 @@ let ``don't indent function application arguments when function name is further b c)))))))))))))))))))))) """ + +[] +let ``type annotation spacing should not be adjusted for previous expression length, 3179`` () = + formatSourceString + """ +let x = + (someFunctionCall()) |> anotherOne |> anotherOne |> alsoAnotherOne + + Unchecked.defaultof<_> +""" + config + |> prepend newline + |> should + equal + """ +let x = + (someFunctionCall ()) |> anotherOne |> anotherOne |> alsoAnotherOne + + Unchecked.defaultof<_> +""" + +[] +let ``type annotation spacing should not be adjusted for previous expression length without blank line, 3179`` () = + formatSourceString + """ +let x = + (someFunctionCall()) |> anotherOne |> anotherOne |> alsoAnotherOne + Unchecked.defaultof<_> +""" + config + |> prepend newline + |> should + equal + """ +let x = + (someFunctionCall ()) |> anotherOne |> anotherOne |> alsoAnotherOne + Unchecked.defaultof<_> +""" + +[] +let ``type annotation spacing should not be adjusted for short previous expression, 3179`` () = + formatSourceString + """ +let x = + someFunction() + + Unchecked.defaultof<_> +""" + config + |> prepend newline + |> should + equal + """ +let x = + someFunction () + + Unchecked.defaultof<_> +""" + +[] +let ``type annotation spacing should not be adjusted for different type applications, 3179`` () = + formatSourceString + """ +let x = + (someFunctionCall()) |> anotherOne |> anotherOne |> alsoAnotherOne + + List.map +""" + config + |> prepend newline + |> should + equal + """ +let x = + (someFunctionCall ()) |> anotherOne |> anotherOne |> alsoAnotherOne + + List.map +""" + +[] +let ``type annotation spacing should not be adjusted for generic type applications, 3179`` () = + formatSourceString + """ +let x = + (someFunctionCall()) |> anotherOne |> anotherOne |> alsoAnotherOne + + Some<'T>.Create +""" + config + |> prepend newline + |> should + equal + """ +let x = + (someFunctionCall ()) |> anotherOne |> anotherOne |> alsoAnotherOne + + Some<'T>.Create +""" + +[] +let ``function application type parameters should still be aligned correctly, 3179`` () = + formatSourceString + """ +let x = someFunction (arg1, arg2) +""" + config + |> prepend newline + |> should + equal + """ +let x = someFunction (arg1, arg2) +""" diff --git a/src/Fantomas.Core.Tests/ModuleTests.fs b/src/Fantomas.Core.Tests/ModuleTests.fs index 3e486d9c7..9a7282548 100644 --- a/src/Fantomas.Core.Tests/ModuleTests.fs +++ b/src/Fantomas.Core.Tests/ModuleTests.fs @@ -1086,3 +1086,27 @@ namespace ``G-Research``.``FSharp X``.``Analyzers Y`` module StringAnalyzers = () """ + +[] +let ``hash directives around access modifier in module, 3188`` () = + formatSourceString + """ +[] +module + #if !MCP + internal + #endif + Fantomas.Core.CodeFormatterImpl +""" + config + |> prepend newline + |> should + equal + """ +[] +module +#if !MCP + internal +#endif + Fantomas.Core.CodeFormatterImpl +""" diff --git a/src/Fantomas.Core.Tests/TestHelpers.fs b/src/Fantomas.Core.Tests/TestHelpers.fs index 616b5e2d8..76c88a3d3 100644 --- a/src/Fantomas.Core.Tests/TestHelpers.fs +++ b/src/Fantomas.Core.Tests/TestHelpers.fs @@ -31,7 +31,7 @@ let formatFSharpString isFsiFile (s: string) config = if formattedCode <> secondFormattedCode then failwith $"The formatted result was not idempotent.\n%s{formattedCode}\n%s{secondFormattedCode}" - printfn "formatted code:\n%s\n" formattedCode + // printfn "formatted code:\n%s\n" formattedCode return formattedCode } diff --git a/src/Fantomas.Core.Tests/packages.lock.json b/src/Fantomas.Core.Tests/packages.lock.json index 9d84872df..0c28e1b60 100644 --- a/src/Fantomas.Core.Tests/packages.lock.json +++ b/src/Fantomas.Core.Tests/packages.lock.json @@ -13,9 +13,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "FsUnit": { "type": "Direct", @@ -132,14 +132,14 @@ "fantomas.core": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "Fantomas.FCS": "[1.0.0, )" } }, "fantomas.fcs": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "System.Collections.Immutable": "[8.0.0, )", "System.Diagnostics.DiagnosticSource": "[8.0.1, )", "System.Memory": "[4.6.0, )", diff --git a/src/Fantomas.Core/AssemblyInfo.fs b/src/Fantomas.Core/AssemblyInfo.fs index 1c3295927..eaf31e663 100644 --- a/src/Fantomas.Core/AssemblyInfo.fs +++ b/src/Fantomas.Core/AssemblyInfo.fs @@ -3,6 +3,7 @@ namespace Fantomas.Core open System.Runtime.CompilerServices [] +[] do () diff --git a/src/Fantomas.Core/CodeFormatterImpl.fs b/src/Fantomas.Core/CodeFormatterImpl.fs index a517ea42f..a8aee59a7 100644 --- a/src/Fantomas.Core/CodeFormatterImpl.fs +++ b/src/Fantomas.Core/CodeFormatterImpl.fs @@ -6,7 +6,9 @@ open Fantomas.FCS.Syntax open Fantomas.FCS.Text open MultipleDefineCombinations -let getSourceText (source: string) : ISourceText = source.TrimEnd() |> SourceText.ofString +let getSourceText (source: string) : ISourceText = + MCPEvents.addEvent MCPEvents.EventKind.SourceTextCreated + source.TrimEnd() |> SourceText.ofString let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * DefineCombination) array> = // First get the syntax tree without any defines @@ -18,20 +20,20 @@ let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * Defin | ParsedInput.ImplFile(ParsedImplFileInput(trivia = { ConditionalDirectives = directives })) | ParsedInput.SigFile(ParsedSigFileInput(trivia = { ConditionalDirectives = directives })) -> directives - match hashDirectives with - | [] -> - async { - let errors = - baseDiagnostics - |> List.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + let errors = + baseDiagnostics + |> List.filter (fun d -> d.Severity = FSharpDiagnosticSeverity.Error) + + if not errors.IsEmpty then + raise (ParseException baseDiagnostics) - if not errors.IsEmpty then - raise (ParseException baseDiagnostics) + MCPEvents.addEvent (MCPEvents.EventKind.ParsedBaseUntypedTree(baseUntypedTree, baseDiagnostics)) - return [| (baseUntypedTree, DefineCombination.Empty) |] - } + match hashDirectives with + | [] -> async { return [| (baseUntypedTree, DefineCombination.Empty) |] } | hashDirectives -> let defineCombinations = Defines.getDefineCombination hashDirectives + MCPEvents.addEvent (MCPEvents.EventKind.SourceCodeHadDefines defineCombinations) defineCombinations |> List.map (fun defineCombination -> @@ -46,6 +48,10 @@ let parse (isSignature: bool) (source: ISourceText) : Async<(ParsedInput * Defin if not errors.IsEmpty then raise (ParseException diagnostics) + MCPEvents.addEvent ( + MCPEvents.EventKind.ParsedUntypedTreeWithDefines(untypedTree, diagnostics, defineCombination) + ) + return (untypedTree, defineCombination) }) |> Async.Parallel @@ -66,12 +72,23 @@ let formatAST ASTTransformer.mkOak (Some sourceText) ast |> Trivia.enrichTree config sourceText ast + MCPEvents.addEvent (MCPEvents.EventKind.CreatedOakRaw oak) + let oak = match cursor with | None -> oak | Some cursor -> Trivia.insertCursor oak cursor - context |> CodePrinter.genFile oak |> Context.dump false + let contextAfter = context |> CodePrinter.genFile oak + + MCPEvents.addEvent ( + contextAfter.WriterEvents + |> Seq.map string + |> String.concat " , " + |> MCPEvents.CollectedEventsAfterCodePrinter + ) + + contextAfter |> Context.dump false let formatDocument (config: FormatConfig) diff --git a/src/Fantomas.Core/CodePrinter.fs b/src/Fantomas.Core/CodePrinter.fs index 71b9ee65a..f95acb54b 100644 --- a/src/Fantomas.Core/CodePrinter.fs +++ b/src/Fantomas.Core/CodePrinter.fs @@ -2084,7 +2084,11 @@ let genTypeApp (addAdditionalColumnOffset: bool) (node: ExprTypeAppNode) (ctx: C +> colGenericTypeParameters node.TypeParameters // we need to make sure each expression in the function application has offset at least greater than // See: https://github.com/fsprojects/fantomas/issues/1611 - +> addFixedSpaces startColumn + // However, for standalone type applications, we don't need this alignment + // Only add fixed spaces when this is called from a function application context (addAdditionalColumnOffset = true) + // AND when we're not in a standalone type application context + // For standalone type applications, we check if there's contentBefore trivia (newlines) which indicates standalone context + +> ifElse (addAdditionalColumnOffset && not node.HasContentBefore) (addFixedSpaces startColumn) sepNone +> genSingleTextNode node.GreaterThan) ctx @@ -4052,9 +4056,24 @@ let genModule (m: ModuleOrNamespaceNode) = +> genAttributes header.Attributes +> genMultipleTextsNode header.LeadingKeyword +> sepSpace - +> genAccessOpt header.Accessibility + +> optSingle + (fun (accessibility: SingleTextNode) -> + let hasTriviaBeforeAccess = accessibility.HasContentBefore + + onlyIf hasTriviaBeforeAccess indent + +> genSingleTextNode accessibility + +> sepSpace + +> onlyIf hasTriviaBeforeAccess unindent) + header.Accessibility +> onlyIf header.IsRecursive (sepSpace +> !-"rec" +> sepSpace) - +> optSingle genIdentListNode header.Name + +> optSingle + (fun (name: IdentListNode) -> + let hasTriviaBeforeName = name.HasContentBefore + + onlyIf hasTriviaBeforeName (indent +> indent) + +> genIdentListNode name + +> onlyIf hasTriviaBeforeName (unindent +> unindent)) + header.Name |> genNode header) +> newline) m.Header diff --git a/src/Fantomas.Core/Fantomas.Core.fsproj b/src/Fantomas.Core/Fantomas.Core.fsproj index 5ab93cf12..045ea2443 100644 --- a/src/Fantomas.Core/Fantomas.Core.fsproj +++ b/src/Fantomas.Core/Fantomas.Core.fsproj @@ -18,10 +18,11 @@ - - + + + diff --git a/src/Fantomas.Core/MCPEvents.fs b/src/Fantomas.Core/MCPEvents.fs new file mode 100644 index 000000000..73db50797 --- /dev/null +++ b/src/Fantomas.Core/MCPEvents.fs @@ -0,0 +1,39 @@ +module internal Fantomas.Core.MCPEvents + +open System +open System.Collections.Concurrent +open Fantomas.FCS.Parse +open SyntaxOak + +[] +type EventKind = + | SourceTextCreated + | ParsedBaseUntypedTree of + baseTree: Fantomas.FCS.Syntax.ParsedInput * + baseDiagnostics: Fantomas.FCS.Parse.FSharpParserDiagnostic list + | SourceCodeHadDefines of combinations: Fantomas.Core.DefineCombination list + | ParsedUntypedTreeWithDefines of + tree: Fantomas.FCS.Syntax.ParsedInput * + diagnostics: Fantomas.FCS.Parse.FSharpParserDiagnostic list * + defineCombination: Fantomas.Core.DefineCombination + | ParseFailed of diagnostics: FSharpParserDiagnostic list + | FoundTrivia of TriviaNode array + | CreatedOakRaw of oak: Oak + | CreatedOak of string + | CollectedEventsAfterCodePrinter of string + | UnExpectedExceptionHappened of exn + +[] +type MCPEvent = { Created: DateTime; Kind: EventKind } + +let capturedEvents = ConcurrentQueue() + +let addEvent eventKind = + capturedEvents.Enqueue( + { Kind = eventKind + Created = DateTime.Now } + ) + +let resetEvents () = + while capturedEvents.TryDequeue() |> fst do + () diff --git a/src/Fantomas.Core/SyntaxOak.fs b/src/Fantomas.Core/SyntaxOak.fs index c15dc6f0d..b2a4ae2d6 100644 --- a/src/Fantomas.Core/SyntaxOak.fs +++ b/src/Fantomas.Core/SyntaxOak.fs @@ -15,6 +15,10 @@ type TriviaNode(content: TriviaContent, range: range) = member val Content = content member val Range = range + override this.ToString() = + sprintf + $"%A{this.Content} (%d{this.Range.StartLine},%d{this.Range.StartColumn} - %d{this.Range.EndLine},%d{this.Range.EndColumn})" + [] type Node = abstract ContentBefore: TriviaNode seq diff --git a/src/Fantomas.Core/Trivia.fs b/src/Fantomas.Core/Trivia.fs index 8ea4141fe..7224e19dc 100644 --- a/src/Fantomas.Core/Trivia.fs +++ b/src/Fantomas.Core/Trivia.fs @@ -337,6 +337,9 @@ let enrichTree (config: FormatConfig) (sourceText: ISourceText) (ast: ParsedInpu [| yield! comments; yield! newlines; yield! directives |] |> Array.sortBy (fun n -> n.Range.Start.Line, n.Range.Start.Column) + if trivia.Length > 0 then + MCPEvents.addEvent (MCPEvents.EventKind.FoundTrivia trivia) + addToTree tree trivia tree diff --git a/src/Fantomas.Core/packages.lock.json b/src/Fantomas.Core/packages.lock.json index c69d664b1..a6940a682 100644 --- a/src/Fantomas.Core/packages.lock.json +++ b/src/Fantomas.Core/packages.lock.json @@ -16,9 +16,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "G-Research.FSharp.Analyzers": { "type": "Direct", @@ -121,7 +121,7 @@ "fantomas.fcs": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "System.Collections.Immutable": "[8.0.0, )", "System.Diagnostics.DiagnosticSource": "[8.0.1, )", "System.Memory": "[4.6.0, )", diff --git a/src/Fantomas.FCS/packages.lock.json b/src/Fantomas.FCS/packages.lock.json index 74229b467..464d42de8 100644 --- a/src/Fantomas.FCS/packages.lock.json +++ b/src/Fantomas.FCS/packages.lock.json @@ -16,9 +16,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "FsLexYacc": { "type": "Direct", diff --git a/src/Fantomas.Tests/packages.lock.json b/src/Fantomas.Tests/packages.lock.json index 1786c3260..c3addd24e 100644 --- a/src/Fantomas.Tests/packages.lock.json +++ b/src/Fantomas.Tests/packages.lock.json @@ -13,9 +13,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "FsUnit": { "type": "Direct", @@ -207,7 +207,7 @@ "type": "Project", "dependencies": { "Argu": "[6.2.4, )", - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "Fantomas.Client": "[1.0.0, )", "Fantomas.Core": "[1.0.0, )", "Ignore": "[0.2.1, )", @@ -224,7 +224,7 @@ "fantomas.client": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "SemanticVersioning": "[2.0.2, )", "StreamJsonRpc": "[2.20.20, )" } @@ -232,14 +232,14 @@ "fantomas.core": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "Fantomas.FCS": "[1.0.0, )" } }, "fantomas.fcs": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "System.Collections.Immutable": "[8.0.0, )", "System.Diagnostics.DiagnosticSource": "[8.0.1, )", "System.Memory": "[4.6.0, )", diff --git a/src/Fantomas/packages.lock.json b/src/Fantomas/packages.lock.json index 7b8b3bebc..4b6ea5cff 100644 --- a/src/Fantomas/packages.lock.json +++ b/src/Fantomas/packages.lock.json @@ -32,9 +32,9 @@ }, "FSharp.Core": { "type": "Direct", - "requested": "[8.0.100, )", - "resolved": "8.0.100", - "contentHash": "ZOVZ/o+jI3ormTZOa28Wh0tSRoyle1f7lKFcUN61sPiXI7eDZu8eSveFybgTeyIEyW0ujjp31cp7GOglDgsNEg==" + "requested": "[9.0.100, )", + "resolved": "9.0.100", + "contentHash": "ye8yagHGsH08H2Twno5GRWkSbrMtxK/SWiHuPcF+3nODpW65/VJ8RO0aWxp8n9+KQbmahg90wAEL3TEXjF0r6A==" }, "G-Research.FSharp.Analyzers": { "type": "Direct", @@ -272,7 +272,7 @@ "fantomas.client": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "SemanticVersioning": "[2.0.2, )", "StreamJsonRpc": "[2.20.20, )" } @@ -280,14 +280,14 @@ "fantomas.core": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "Fantomas.FCS": "[1.0.0, )" } }, "fantomas.fcs": { "type": "Project", "dependencies": { - "FSharp.Core": "[8.0.100, )", + "FSharp.Core": "[9.0.100, )", "System.Collections.Immutable": "[8.0.0, )", "System.Diagnostics.DiagnosticSource": "[8.0.1, )", "System.Memory": "[4.6.0, )",