diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7055893f1..8f391fa46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,11 +90,15 @@ jobs: run: dotnet tool restore - name: Check format - run: dotnet fantomas --check src + run: dotnet fantomas --check build.fsx src env: DOTNET_ROLL_FORWARD: LatestMajor DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 + # Ensure the scaffolding code can still add items to the existing code. + - name: EnsureCanScaffoldCodeFix + run: dotnet fsi build.fsx -- -p EnsureCanScaffoldCodeFix + - name: Run Build run: dotnet build -c Release env: diff --git a/FsAutoComplete.sln b/FsAutoComplete.sln index 5aed2b731..880144ac3 100644 --- a/FsAutoComplete.sln +++ b/FsAutoComplete.sln @@ -25,8 +25,6 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "OptionAnalyzer", "test\Opti EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsAutoComplete.DependencyManager.Dummy", "test\FsAutoComplete.DependencyManager.Dummy\FsAutoComplete.DependencyManager.Dummy.fsproj", "{C58701B0-D8E3-4B68-A7DE-8524C95F86C0}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "build", "build\build.fsproj", "{400D56D0-28C9-4210-AA30-BD688122E298}" -EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "benchmarks", "benchmarks\benchmarks.fsproj", "{0CD029D8-B39E-4CBE-A190-C84A7A811180}" EndProject Global @@ -63,10 +61,6 @@ Global {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Debug|Any CPU.Build.0 = Debug|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C58701B0-D8E3-4B68-A7DE-8524C95F86C0}.Release|Any CPU.Build.0 = Release|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Debug|Any CPU.Build.0 = Debug|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Release|Any CPU.ActiveCfg = Release|Any CPU - {400D56D0-28C9-4210-AA30-BD688122E298}.Release|Any CPU.Build.0 = Release|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Debug|Any CPU.Build.0 = Debug|Any CPU {0CD029D8-B39E-4CBE-A190-C84A7A811180}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 26d5d7b0c..000000000 --- a/build.cmd +++ /dev/null @@ -1,2 +0,0 @@ -dotnet tool restore -dotnet build diff --git a/build.fsx b/build.fsx new file mode 100644 index 000000000..1a948bd7a --- /dev/null +++ b/build.fsx @@ -0,0 +1,446 @@ +#r "nuget: Fun.Build, 1.1.2" +#r "nuget: Fake.Tools.Git, 6.0.0" +#r "nuget: Fake.IO.FileSystem, 6.0.0" +#r "nuget: Fantomas.Core, 6.3.1" + +open Fun.Build +open Fake.Tools + +module ScaffoldCodeFix = + open System + open System.IO + open Fake.Core + open Fake.IO.FileSystemOperators + open Fantomas.Core.SyntaxOak + + let repositoryRoot = __SOURCE_DIRECTORY__ + + let AdaptiveServerStatePath = + repositoryRoot + "src" + "FsAutoComplete" + "LspServers" + "AdaptiveServerState.fs" + + + let TestsPath = + repositoryRoot + "test" + "FsAutoComplete.Tests.Lsp" + "CodeFixTests" + "Tests.fs" + + let removeReturnCarriage (v: string) = v.Replace("\r", "") + + let mkCodeFixImplementation codeFixName = + let path = + repositoryRoot + "src" + "FsAutoComplete" + "CodeFixes" + $"{codeFixName}.fs" + + let content = + $"""module FsAutoComplete.CodeFix.%s{codeFixName} + +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FsToolkit.ErrorHandling +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.CodeFix.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers + +// The syntax tree can be an intimidating set of types to work with. +// It is a tree structure but it consists out of many different types. +// See https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-syntax.html +// It can be useful to inspect a syntax tree via a code sample using https://fsprojects.github.io/fantomas-tools/#/ast +// For example `let a b c = ()` in +// https://fsprojects.github.io/fantomas-tools/#/ast?data=N4KABGBEAmCmBmBLAdrAzpAXFSAacUiaAYmolmPAIYA2as%%2BEkAxgPZwWQ2wAuYVYAEZhmYALxgAFAEo8BSLAAeAByrJoFHgCcArrBABfIA +// Let's say we want to find the (FCS) range for identifier `a`. +let visitSyntaxTree + (cursor: FSharp.Compiler.Text.pos) + (tree: ParsedInput) + = + // We will use a syntax visitor to traverse the tree from the top to the node of interest. + // See https://github.com/dotnet/fsharp/blob/main/src/Compiler/Service/ServiceParseTreeWalk.fsi + // We implement the different members of interest and allow the default traversal to move to the lower levels we care about. + let visitor = + // A visitor will report the first item it finds. + // Think of it as `List.tryPick` + // It is not an ideal solution to find all nodes inside a tree, be aware of that. + // For example finding all function names. + {{ new SyntaxVisitorBase() with + // We know that `a` will be part of a `SynPat.LongIdent` + // This was visible in the online tool. + member _.VisitPat(path, defaultTraverse, synPat) = + match synPat with + | SynPat.LongIdent(longDotId = SynLongIdent(id = [ functionNameIdent ])) -> + // When our code fix operates on the user's code there is no way of knowing what will be inside the syntax tree. + // So we need to be careful and verify that the pattern is indeed matching the position of the cursor. + if FSharp.Compiler.Text.Range.rangeContainsPos functionNameIdent.idRange cursor then + Some functionNameIdent.idRange + else + None + | _ -> None }} + + // Invoke the visitor and kick off the traversal. + SyntaxTraversal.Traverse(cursor, tree, visitor) + +// TODO: add proper title for code fix +let title = "%s{codeFixName} Codefix" + +let fix + (getParseResultsForFile: GetParseResultsForFile) + : CodeFix = + fun (codeActionParams: CodeActionParams) -> + asyncResult {{ + // Most code fixes have some general setup. + // We initially want to detect the state of the current code and whether we can propose any text edits to the user. + + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + // The converted LSP start position to an FCS start position. + let fcsPos = protocolPosToPos codeActionParams.Range.Start + // The syntax tree and typed tree, current line and sourceText of the current file. + let! (parseAndCheckResults:ParseAndCheckResults, line:string, sourceText:IFSACSourceText) = + getParseResultsForFile fileName fcsPos + + // As an example, we want to check whether the users cursor is inside a function definition name. + // We will traverse the syntax tree to verify this is the case. + match visitSyntaxTree fcsPos parseAndCheckResults.GetParseResults.ParseTree with + | None -> + // The cursor is not in a position we are interested in. + // This code fix should not trigger any suggestions so we return an empty list. + return [] + | Some mBindingName -> + // It turns out we are inside a let binding and we have the range of the function name. + // Just for fun, we want to detect if there is a matching typed tree symbol present for the current name. + // We could have passed the function name from the syntax visitor, instead will we grab it from the source text. + let! functionName = sourceText.GetText mBindingName + // FSharpSymbolUse is reflecting the typed tree. + // See https://fsharp.github.io/fsharp-compiler-docs/fcs/symbols.html + let symbolUse: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse option = + parseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation(mBindingName.EndLine, mBindingName.EndColumn, line, [ functionName ]) + + let hasFunctionDefinitionSymbol = + match symbolUse with + | None -> false + | Some symbolUse -> + // We want to verify the found symbol is indeed a definition of a function + match symbolUse.Symbol with + | :? FSharpMemberOrFunctionOrValue -> true + | _ -> false + + if not hasFunctionDefinitionSymbol then + return [] + else + // Return a list of Fix records for when the code fix is applicable. + return [ + {{ + SourceDiagnostic = None + Title = title + File = codeActionParams.TextDocument + // Based on conditional logic, you typically want to suggest a text edit to the user. + Edits = [| + {{ + // When dealing with FCS, we typically want to use the FCS flavour of range. + // However, to interact correctly with the LSP protocol, we need to return an LSP range. + Range = fcsRangeToLsp mBindingName + NewText = "Text replaced by %s{codeFixName}" + }} + |] + Kind = FixKind.Fix + }} + ] + }} +""" + + File.WriteAllText(path, removeReturnCarriage content) + Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" + + let mkCodeFixSignature codeFixName = + let path = + repositoryRoot + "src" + "FsAutoComplete" + "CodeFixes" + $"{codeFixName}.fsi" + + let content = + $"""module FsAutoComplete.CodeFix.%s{codeFixName} + +open FsAutoComplete.CodeFix.Types + +val title: string +val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix +""" + + File.WriteAllText(path, removeReturnCarriage content) + Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" + + let updateProjectFiles () = + let fsAutoCompleteProject = + repositoryRoot "src" "FsAutoComplete" "FsAutoComplete.fsproj" + + File.SetLastWriteTime(fsAutoCompleteProject, DateTime.Now) + + let fsAutoCompleteTestsLsp = + repositoryRoot + "test" + "FsAutoComplete.Tests.Lsp" + "FsAutoComplete.Tests.Lsp.fsproj" + + File.SetLastWriteTime(fsAutoCompleteTestsLsp, DateTime.Now) + + let (|IdentName|_|) (name: string) (identListNode: IdentListNode) = + match identListNode.Content with + | [ IdentifierOrDot.Ident stn ] when stn.Text = name -> Some() + | _ -> None + + let getOakFor path = + let content = File.ReadAllText path + + Fantomas.Core.CodeFormatter.ParseOakAsync(false, content) + |> Async.RunSynchronously + |> Array.head + |> fst + + let appendItemToArrayOrList item path (node: ExprArrayOrListNode) = + let lastElement = node.Elements |> List.last |> Expr.Node + let startIndent = lastElement.Range.StartColumn + let lineIdx = lastElement.Range.EndLine - 1 + let arrayEndsOnLastElement = node.Range.EndLine = lastElement.Range.EndLine + + let updatedLines = + let lines = File.ReadAllLines path + let currentLastLine = lines.[lineIdx] + let spaces = String.replicate startIndent " " + + if arrayEndsOnLastElement then + let endOfLastElement = currentLastLine.Substring(0, lastElement.Range.EndColumn) + let endOfArray = currentLastLine.Substring(lastElement.Range.EndColumn) + + lines + |> Array.updateAt lineIdx $"{endOfLastElement}\n%s{spaces}%s{item}%s{endOfArray}" + else + lines |> Array.insertAt (lineIdx + 1) $"%s{spaces}%s{item}" + + let content = String.concat "\n" updatedLines + File.WriteAllText(path, content) + Trace.tracefn $"Added \"%s{item}\" to %s{Path.GetRelativePath(repositoryRoot, path)}" + + module List = + let exactlyOneOrFail (message: string) (items: 'T list) : 'T = + if items.Length = 1 then items.Head else failwith message + + let pickOrFail (message: string) (chooser: 'T -> 'U option) (items: 'T list) : 'U = + match List.tryPick chooser items with + | None -> failwith message + | Some u -> u + + let findArrayOrListOfFail (e: Expr) = + match e with + | Expr.ArrayOrList array -> array + | e -> failwithf $"Expected to find Expr.ArrayOrList, got %A{e}" + + let findTypeWithNameOfFail (typeName: string) (mn: ModuleOrNamespaceNode) : ITypeDefn = + mn.Declarations + |> List.pickOrFail $"Expected to find ModuleDecl.TypeDefn for %s{typeName}" (function + | ModuleDecl.TypeDefn t -> + let tdn = TypeDefn.TypeDefnNode t + + match tdn.TypeName.Identifier with + | IdentName typeName -> Some tdn + | _ -> None + | _ -> None) + + let findArrayInAdaptiveFSharpLspServer () : ExprArrayOrListNode = + let oak = getOakFor AdaptiveServerStatePath + + // namespace FsAutoComplete.Lsp + let ns = + oak.ModulesOrNamespaces + |> List.exactlyOneOrFail "Expected a single namespace in Oak." + + // type AdaptiveState + let t = findTypeWithNameOfFail "AdaptiveState" ns + + // let codefixes = + let codefixesValue = + t.Members + |> List.pickOrFail "Expected to find MemberDefn.LetBinding for codefixes" (function + | MemberDefn.LetBinding bindingList -> + match bindingList.Bindings with + | bindings -> + bindings + |> List.tryPick (fun binding -> + match binding.FunctionName with + | Choice1Of2(IdentName "codefixes") -> Some binding + | _ -> None) + | _ -> None) + + let infixApp = + match codefixesValue.Expr with + | Expr.CompExprBody body -> + match List.last body.Statements with + | ComputationExpressionStatement.OtherStatement other -> + match other with + | Expr.InfixApp infixApp -> infixApp + | e -> failwithf $"Expected to find Expr.InfixApp, got %A{e}" + | ces -> failwithf $"Expected to find ComputationExpressionStatement.OtherStatement, got %A{ces}" + | e -> failwithf $"Expected to find Expr.CompExprBody, got %A{e}" + + let appWithLambda = + match infixApp.RightHandSide with + | Expr.AppWithLambda appWithLambda -> appWithLambda + | e -> failwithf $"Expected to find Expr.AppWithLambda, got %A{e}" + + let lambda = + match appWithLambda.Lambda with + | Choice1Of2 lambda -> lambda + | Choice2Of2 ml -> failwithf $"Expected to find ExprLambdaNode, got %A{ml}" + + findArrayOrListOfFail lambda.Expr + + let wireCodeFixInAdaptiveFSharpLspServer codeFixName = + try + let array = findArrayInAdaptiveFSharpLspServer () + + appendItemToArrayOrList $"%s{codeFixName}.fix tryGetParseAndCheckResultsForFile" AdaptiveServerStatePath array + with ex -> + Trace.traceException ex + + Trace.traceError + $"Unable to find array of codefixes in %s{AdaptiveServerStatePath}.\nDid the code structure change?" + + + let mkCodeFixTests codeFixName = + let path = + repositoryRoot + "test" + "FsAutoComplete.Tests.Lsp" + "CodeFixTests" + $"%s{codeFixName}Tests.fs" + + let contents = + $"module private FsAutoComplete.Tests.CodeFixTests.%s{codeFixName}Tests + +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix + +let tests state = + serverTestList (nameof %s{codeFixName}) state defaultConfigDto None (fun server -> + [ let selectCodeFix = CodeFix.withTitle %s{codeFixName}.title + + ftestCaseAsync \"first unit test for %s{codeFixName}\" + <| CodeFix.check + server + \"let a$0 b c = ()\" + Diagnostics.acceptAll + selectCodeFix + \"let Text replaced by %s{codeFixName} b c = ()\" + ]) +" + + File.WriteAllText(path, removeReturnCarriage contents) + Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" + + let findListInTests () = + let oak = getOakFor TestsPath + // module FsAutoComplete.Tests.CodeFixTests.Tests + let testsModule = + oak.ModulesOrNamespaces + |> List.exactlyOneOrFail "Expected a single module in Oak." + + // let tests state = + let testBinding = + testsModule.Declarations + |> List.pickOrFail "Expected to find ModuleDecl.TopLevelBinding for tests" (function + | ModuleDecl.TopLevelBinding binding -> + match binding.FunctionName with + | Choice1Of2(IdentName "tests") -> Some binding + | _ -> None + | _ -> None) + + let appNode = + match testBinding.Expr with + | Expr.App appNode -> appNode + | e -> failwithf $"Expected Expr.App, got %A{e}" + + findArrayOrListOfFail (List.last appNode.Arguments) + + let wireCodeFixTests codeFixName = + try + let list = findListInTests () + appendItemToArrayOrList $"%s{codeFixName}Tests.tests state" TestsPath list + with ex -> + Trace.traceException ex + Trace.traceError $"Unable to find array of tests in %s{TestsPath}.\nDid the code structure change?" + + let scaffold (codeFixName: string) : unit = + // generate files in src/CodeFixes/ + mkCodeFixImplementation codeFixName + mkCodeFixSignature codeFixName + + // Wire up codefix to LSP servers + wireCodeFixInAdaptiveFSharpLspServer codeFixName + + // Add test file + mkCodeFixTests codeFixName + + // Wire up tests in test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs + wireCodeFixTests codeFixName + + updateProjectFiles () + Trace.tracefn $"Scaffolding %s{codeFixName} complete!" + + let ensureScaffoldStillWorks () = + findArrayInAdaptiveFSharpLspServer () |> ignore + findListInTests () |> ignore + +pipeline "EnsureRepoConfig" { + description "Configure custom git hooks, currently only used to ensure that code is formatted before pushing" + workingDir __SOURCE_DIRECTORY__ + stage "Git" { run (fun _ -> Git.CommandHelper.gitCommand "" "config core.hooksPath .githooks") } + runIfOnlySpecified true +} + +pipeline "ScaffoldCodeFix" { + description "Scaffold a new code fix." + workingDir __SOURCE_DIRECTORY__ + + stage "Scaffold" { + run (fun ctx -> + let codeFixName = ctx.GetAllCmdArgs() |> List.tryLast + + match codeFixName with + | None -> printfn "Usage: dotnet fsi build.fsx -- -p ScaffoldCodeFix " + | Some codeFixName -> ScaffoldCodeFix.scaffold codeFixName) + } + + runIfOnlySpecified true +} + +pipeline "EnsureCanScaffoldCodeFix" { + description "Ensure the ScaffoldCodeFix pipeline can still be executed." + workingDir __SOURCE_DIRECTORY__ + stage "Ensure" { run (fun _ -> ScaffoldCodeFix.ensureScaffoldStillWorks ()) } + runIfOnlySpecified true +} + +pipeline "Build" { + description "Default build pipeline" + workingDir __SOURCE_DIRECTORY__ + + stage "Build" { + run "dotnet tool restore" + run "dotnet build" + } + + runIfOnlySpecified false +} + +tryPrintPipelineCommandHelp () diff --git a/build.sh b/build.sh deleted file mode 100755 index a0a6f01cf..000000000 --- a/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -dotnet tool restore -dotnet build diff --git a/build/Program.fs b/build/Program.fs deleted file mode 100644 index 79a864066..000000000 --- a/build/Program.fs +++ /dev/null @@ -1,35 +0,0 @@ -open Fake.Core -open Fake.IO -open Fake.Tools - -System.Environment.CurrentDirectory <- (Path.combine __SOURCE_DIRECTORY__ "..") - -let init args = - let execContext = Context.FakeExecutionContext.Create false "build.fsx" args - Context.setExecutionContext (Context.RuntimeContext.Fake execContext) - Target.initEnvironment () - - Target.create "EnsureRepoConfig" (fun _ -> - // Configure custom git hooks - // * Currently only used to ensure that code is formatted before pushing - Git.CommandHelper.gitCommand "" "config core.hooksPath .githooks") - - Target.create "ScaffoldCodeFix" (fun ctx -> - let codeFixName = ctx.Context.Arguments |> List.tryHead - - match codeFixName with - | None -> failwith "Usage: dotnet run --project ./build/build.fsproj -- -t ScaffoldCodeFix " - | Some codeFixName -> ScaffoldCodeFix.scaffold codeFixName) - - Target.create "EnsureCanScaffoldCodeFix" (fun _ -> ScaffoldCodeFix.ensureScaffoldStillWorks ()) - -[] -let main args = - init (args |> List.ofArray) - - try - Target.runOrDefaultWithArguments "EnsureCanScaffoldCodeFix" - 0 - with e -> - printfn "%A" e - 1 diff --git a/build/ScaffoldCodeFix.fs b/build/ScaffoldCodeFix.fs deleted file mode 100644 index 71b35504f..000000000 --- a/build/ScaffoldCodeFix.fs +++ /dev/null @@ -1,395 +0,0 @@ -module ScaffoldCodeFix - -open System -open System.IO -open Fake.Core -open Fake.IO.FileSystemOperators -open Fantomas.Core.SyntaxOak - -let repositoryRoot = __SOURCE_DIRECTORY__ ".." - -let AdaptiveServerStatePath = - repositoryRoot - "src" - "FsAutoComplete" - "LspServers" - "AdaptiveServerState.fs" - - -let TestsPath = - repositoryRoot - "test" - "FsAutoComplete.Tests.Lsp" - "CodeFixTests" - "Tests.fs" - -let removeReturnCarriage (v: string) = v.Replace("\r", "") - -let mkCodeFixImplementation codeFixName = - let path = - repositoryRoot - "src" - "FsAutoComplete" - "CodeFixes" - $"{codeFixName}.fs" - - let content = - $"""module FsAutoComplete.CodeFix.%s{codeFixName} - -open FSharp.Compiler.Symbols -open FSharp.Compiler.Syntax -open FsToolkit.ErrorHandling -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete.CodeFix.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers - -// The syntax tree can be an intimidating set of types to work with. -// It is a tree structure but it consists out of many different types. -// See https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-syntax.html -// It can be useful to inspect a syntax tree via a code sample using https://fsprojects.github.io/fantomas-tools/#/ast -// For example `let a b c = ()` in -// https://fsprojects.github.io/fantomas-tools/#/ast?data=N4KABGBEAmCmBmBLAdrAzpAXFSAacUiaAYmolmPAIYA2as%%2BEkAxgPZwWQ2wAuYVYAEZhmYALxgAFAEo8BSLAAeAByrJoFHgCcArrBABfIA -// Let's say we want to find the (FCS) range for identifier `a`. -let visitSyntaxTree - (cursor: FSharp.Compiler.Text.pos) - (tree: ParsedInput) - = - // We will use a syntax visitor to traverse the tree from the top to the node of interest. - // See https://github.com/dotnet/fsharp/blob/main/src/Compiler/Service/ServiceParseTreeWalk.fsi - // We implement the different members of interest and allow the default traversal to move to the lower levels we care about. - let visitor = - // A visitor will report the first item it finds. - // Think of it as `List.tryPick` - // It is not an ideal solution to find all nodes inside a tree, be aware of that. - // For example finding all function names. - {{ new SyntaxVisitorBase() with - // We know that `a` will be part of a `SynPat.LongIdent` - // This was visible in the online tool. - member _.VisitPat(path, defaultTraverse, synPat) = - match synPat with - | SynPat.LongIdent(longDotId = SynLongIdent(id = [ functionNameIdent ])) -> - // When our code fix operates on the user's code there is no way of knowing what will be inside the syntax tree. - // So we need to be careful and verify that the pattern is indeed matching the position of the cursor. - if FSharp.Compiler.Text.Range.rangeContainsPos functionNameIdent.idRange cursor then - Some functionNameIdent.idRange - else - None - | _ -> None }} - - // Invoke the visitor and kick off the traversal. - SyntaxTraversal.Traverse(cursor, tree, visitor) - -// TODO: add proper title for code fix -let title = "%s{codeFixName} Codefix" - -let fix - (getParseResultsForFile: GetParseResultsForFile) - : CodeFix = - fun (codeActionParams: CodeActionParams) -> - asyncResult {{ - // Most code fixes have some general setup. - // We initially want to detect the state of the current code and whether we can propose any text edits to the user. - - let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - // The converted LSP start position to an FCS start position. - let fcsPos = protocolPosToPos codeActionParams.Range.Start - // The syntax tree and typed tree, current line and sourceText of the current file. - let! (parseAndCheckResults:ParseAndCheckResults, line:string, sourceText:IFSACSourceText) = - getParseResultsForFile fileName fcsPos - - // As an example, we want to check whether the users cursor is inside a function definition name. - // We will traverse the syntax tree to verify this is the case. - match visitSyntaxTree fcsPos parseAndCheckResults.GetParseResults.ParseTree with - | None -> - // The cursor is not in a position we are interested in. - // This code fix should not trigger any suggestions so we return an empty list. - return [] - | Some mBindingName -> - // It turns out we are inside a let binding and we have the range of the function name. - // Just for fun, we want to detect if there is a matching typed tree symbol present for the current name. - // We could have passed the function name from the syntax visitor, instead will we grab it from the source text. - let! functionName = sourceText.GetText mBindingName - // FSharpSymbolUse is reflecting the typed tree. - // See https://fsharp.github.io/fsharp-compiler-docs/fcs/symbols.html - let symbolUse: FSharp.Compiler.CodeAnalysis.FSharpSymbolUse option = - parseAndCheckResults.GetCheckResults.GetSymbolUseAtLocation(mBindingName.EndLine, mBindingName.EndColumn, line, [ functionName ]) - - let hasFunctionDefinitionSymbol = - match symbolUse with - | None -> false - | Some symbolUse -> - // We want to verify the found symbol is indeed a definition of a function - match symbolUse.Symbol with - | :? FSharpMemberOrFunctionOrValue -> true - | _ -> false - - if not hasFunctionDefinitionSymbol then - return [] - else - // Return a list of Fix records for when the code fix is applicable. - return [ - {{ - SourceDiagnostic = None - Title = title - File = codeActionParams.TextDocument - // Based on conditional logic, you typically want to suggest a text edit to the user. - Edits = [| - {{ - // When dealing with FCS, we typically want to use the FCS flavour of range. - // However, to interact correctly with the LSP protocol, we need to return an LSP range. - Range = fcsRangeToLsp mBindingName - NewText = "Text replaced by %s{codeFixName}" - }} - |] - Kind = FixKind.Fix - }} - ] - }} -""" - - File.WriteAllText(path, removeReturnCarriage content) - Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" - -let mkCodeFixSignature codeFixName = - let path = - repositoryRoot - "src" - "FsAutoComplete" - "CodeFixes" - $"{codeFixName}.fsi" - - let content = - $"""module FsAutoComplete.CodeFix.%s{codeFixName} - -open FsAutoComplete.CodeFix.Types - -val title: string -val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix -""" - - File.WriteAllText(path, removeReturnCarriage content) - Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" - -let updateProjectFiles () = - let fsAutoCompleteProject = - repositoryRoot "src" "FsAutoComplete" "FsAutoComplete.fsproj" - - File.SetLastWriteTime(fsAutoCompleteProject, DateTime.Now) - - let fsAutoCompleteTestsLsp = - repositoryRoot - "test" - "FsAutoComplete.Tests.Lsp" - "FsAutoComplete.Tests.Lsp.fsproj" - - File.SetLastWriteTime(fsAutoCompleteTestsLsp, DateTime.Now) - -let (|IdentName|_|) (name: string) (identListNode: IdentListNode) = - match identListNode.Content with - | [ IdentifierOrDot.Ident stn ] when stn.Text = name -> Some() - | _ -> None - -let getOakFor path = - let content = File.ReadAllText path - - Fantomas.Core.CodeFormatter.ParseOakAsync(false, content) - |> Async.RunSynchronously - |> Array.head - |> fst - -let appendItemToArrayOrList item path (node: ExprArrayOrListNode) = - let lastElement = node.Elements |> List.last |> Expr.Node - let startIndent = lastElement.Range.StartColumn - let lineIdx = lastElement.Range.EndLine - 1 - let arrayEndsOnLastElement = node.Range.EndLine = lastElement.Range.EndLine - - let updatedLines = - let lines = File.ReadAllLines path - let currentLastLine = lines.[lineIdx] - let spaces = String.replicate startIndent " " - - if arrayEndsOnLastElement then - let endOfLastElement = currentLastLine.Substring(0, lastElement.Range.EndColumn) - let endOfArray = currentLastLine.Substring(lastElement.Range.EndColumn) - - lines - |> Array.updateAt lineIdx $"{endOfLastElement}\n%s{spaces}%s{item}%s{endOfArray}" - else - lines |> Array.insertAt (lineIdx + 1) $"%s{spaces}%s{item}" - - let content = String.concat "\n" updatedLines - File.WriteAllText(path, content) - Trace.tracefn $"Added \"%s{item}\" to %s{Path.GetRelativePath(repositoryRoot, path)}" - -module List = - let exactlyOneOrFail (message: string) (items: 'T list) : 'T = - if items.Length = 1 then items.Head else failwith message - - let pickOrFail (message: string) (chooser: 'T -> 'U option) (items: 'T list) : 'U = - match List.tryPick chooser items with - | None -> failwith message - | Some u -> u - -let findArrayOrListOfFail (e: Expr) = - match e with - | Expr.ArrayOrList array -> array - | e -> failwithf $"Expected to find Expr.ArrayOrList, got %A{e}" - -let findTypeWithNameOfFail (typeName: string) (mn: ModuleOrNamespaceNode) : ITypeDefn = - mn.Declarations - |> List.pickOrFail $"Expected to find ModuleDecl.TypeDefn for %s{typeName}" (function - | ModuleDecl.TypeDefn t -> - let tdn = TypeDefn.TypeDefnNode t - - match tdn.TypeName.Identifier with - | IdentName typeName -> Some tdn - | _ -> None - | _ -> None) - -let findArrayInAdaptiveFSharpLspServer () : ExprArrayOrListNode = - let oak = getOakFor AdaptiveServerStatePath - - // namespace FsAutoComplete.Lsp - let ns = - oak.ModulesOrNamespaces - |> List.exactlyOneOrFail "Expected a single namespace in Oak." - - // type AdaptiveState - let t = findTypeWithNameOfFail "AdaptiveState" ns - - // let codefixes = - let codefixesValue = - t.Members - |> List.pickOrFail "Expected to find MemberDefn.LetBinding for codefixes" (function - | MemberDefn.LetBinding bindingList -> - match bindingList.Bindings with - | bindings -> - bindings - |> List.tryPick (fun binding -> - match binding.FunctionName with - | Choice1Of2(IdentName "codefixes") -> Some binding - | _ -> None) - | _ -> None) - - let infixApp = - match codefixesValue.Expr with - | Expr.CompExprBody body -> - match List.last body.Statements with - | ComputationExpressionStatement.OtherStatement other -> - match other with - | Expr.InfixApp infixApp -> infixApp - | e -> failwithf $"Expected to find Expr.InfixApp, got %A{e}" - | ces -> failwithf $"Expected to find ComputationExpressionStatement.OtherStatement, got %A{ces}" - | e -> failwithf $"Expected to find Expr.CompExprBody, got %A{e}" - - let appWithLambda = - match infixApp.RightHandSide with - | Expr.AppWithLambda appWithLambda -> appWithLambda - | e -> failwithf $"Expected to find Expr.AppWithLambda, got %A{e}" - - let lambda = - match appWithLambda.Lambda with - | Choice1Of2 lambda -> lambda - | Choice2Of2 ml -> failwithf $"Expected to find ExprLambdaNode, got %A{ml}" - - findArrayOrListOfFail lambda.Expr - -let wireCodeFixInAdaptiveFSharpLspServer codeFixName = - try - let array = findArrayInAdaptiveFSharpLspServer () - - appendItemToArrayOrList $"%s{codeFixName}.fix tryGetParseAndCheckResultsForFile" AdaptiveServerStatePath array - with ex -> - Trace.traceException ex - - Trace.traceError - $"Unable to find array of codefixes in %s{AdaptiveServerStatePath}.\nDid the code structure change?" - - -let mkCodeFixTests codeFixName = - let path = - repositoryRoot - "test" - "FsAutoComplete.Tests.Lsp" - "CodeFixTests" - $"%s{codeFixName}Tests.fs" - - let contents = - $"module private FsAutoComplete.Tests.CodeFixTests.%s{codeFixName}Tests - -open Expecto -open Helpers -open Utils.ServerTests -open Utils.CursorbasedTests -open FsAutoComplete.CodeFix - -let tests state = - serverTestList (nameof %s{codeFixName}) state defaultConfigDto None (fun server -> - [ let selectCodeFix = CodeFix.withTitle %s{codeFixName}.title - - ftestCaseAsync \"first unit test for %s{codeFixName}\" - <| CodeFix.check - server - \"let a$0 b c = ()\" - Diagnostics.acceptAll - selectCodeFix - \"let Text replaced by %s{codeFixName} b c = ()\" - ]) -" - - File.WriteAllText(path, removeReturnCarriage contents) - Trace.tracefn $"Generated %s{Path.GetRelativePath(repositoryRoot, path)}" - -let findListInTests () = - let oak = getOakFor TestsPath - // module FsAutoComplete.Tests.CodeFixTests.Tests - let testsModule = - oak.ModulesOrNamespaces - |> List.exactlyOneOrFail "Expected a single module in Oak." - - // let tests state = - let testBinding = - testsModule.Declarations - |> List.pickOrFail "Expected to find ModuleDecl.TopLevelBinding for tests" (function - | ModuleDecl.TopLevelBinding binding -> - match binding.FunctionName with - | Choice1Of2(IdentName "tests") -> Some binding - | _ -> None - | _ -> None) - - let appNode = - match testBinding.Expr with - | Expr.App appNode -> appNode - | e -> failwithf $"Expected Expr.App, got %A{e}" - - findArrayOrListOfFail (List.last appNode.Arguments) - -let wireCodeFixTests codeFixName = - try - let list = findListInTests () - appendItemToArrayOrList $"%s{codeFixName}Tests.tests state" TestsPath list - with ex -> - Trace.traceException ex - Trace.traceError $"Unable to find array of tests in %s{TestsPath}.\nDid the code structure change?" - -let scaffold (codeFixName: string) : unit = - // generate files in src/CodeFixes/ - mkCodeFixImplementation codeFixName - mkCodeFixSignature codeFixName - - // Wire up codefix to LSP servers - wireCodeFixInAdaptiveFSharpLspServer codeFixName - - // Add test file - mkCodeFixTests codeFixName - - // Wire up tests in test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs - wireCodeFixTests codeFixName - - updateProjectFiles () - Trace.tracefn $"Scaffolding %s{codeFixName} complete!" - -let ensureScaffoldStillWorks () = - findArrayInAdaptiveFSharpLspServer () |> ignore - findListInTests () |> ignore diff --git a/build/ScaffoldCodeFix.fsi b/build/ScaffoldCodeFix.fsi deleted file mode 100644 index 5841a7d55..000000000 --- a/build/ScaffoldCodeFix.fsi +++ /dev/null @@ -1,14 +0,0 @@ -module ScaffoldCodeFix - -/// Scaffold a new CodeFix by: -/// - Generating the implementation and signature files. -/// - Wire up the codefix AdaptiveFSharpLspServer.fs -/// - Generate a tests file with a focused test. -/// - Wire up the tests file. -/// - Update the last write time the project files. -val scaffold: codeFixName: string -> unit - -/// Verifies that the code fix scaffold target can still wire up a new codefix to the existing list. -/// Throws when any expected AST nodes can no longer be found. -/// If this code throws, you may need to revisit ScaffoldCodeFix.fs to tweak any recent changes. -val ensureScaffoldStillWorks: unit -> unit diff --git a/build/build.fsproj b/build/build.fsproj deleted file mode 100644 index 071fbd2be..000000000 --- a/build/build.fsproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - Exe - net7.0 - false - - - - - - - - diff --git a/build/paket.references b/build/paket.references deleted file mode 100644 index adbe00212..000000000 --- a/build/paket.references +++ /dev/null @@ -1,5 +0,0 @@ -group Build -Fake.Core.Target -Fake.IO.FileSystem -Fake.Tools.Git -Fantomas.Core diff --git a/docs/Creating a new code fix.md b/docs/Creating a new code fix.md index 9e2844286..88d38b032 100644 --- a/docs/Creating a new code fix.md +++ b/docs/Creating a new code fix.md @@ -15,7 +15,7 @@ To introduce a new code fix within the context of FSAutocomplete, there are seve To streamline the process of creating a new code fix, a convenient `FAKE` target has been provided. By executing the following command: ```bash -dotnet run --project ./build/build.fsproj -- -t ScaffoldCodeFix YourCodeFixName +dotnet fsi build.fsx -- -p ScaffoldCodeFix YourCodeFixName ``` The above command accomplishes the following tasks: diff --git a/paket.dependencies b/paket.dependencies index 5a4c48e05..739581137 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -61,15 +61,3 @@ nuget CommunityToolkit.HighPerformance nuget System.Security.Cryptography.Pkcs 6.0.4 nuget System.Net.Http 4.3.4 # pinned for security reasons nuget System.Text.RegularExpressions 4.3.1 # pinned for security reasons - - -group Build - source https://api.nuget.org/v3/index.json - storage: none - - framework: net7.0 - - nuget Fake.Core.Target - nuget Fake.IO.FileSystem - nuget Fake.Tools.Git - nuget Fantomas.Core 6.2.0 diff --git a/paket.lock b/paket.lock index 789e6ccb9..60780d5e9 100644 --- a/paket.lock +++ b/paket.lock @@ -770,80 +770,3 @@ NUGET Expecto (>= 10.0 < 11.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) FSharp.Core (>= 7.0.200) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) System.Collections.Immutable (>= 6.0) - restriction: || (== net6.0) (== net7.0) (== net8.0) (&& (== netstandard2.0) (>= net6.0)) (&& (== netstandard2.1) (>= net6.0)) - -GROUP Build -STORAGE: NONE -RESTRICTION: == net7.0 -NUGET - remote: https://api.nuget.org/v3/index.json - Fake.Core.CommandLineParsing (6.0) - FParsec (>= 1.1.1) - FSharp.Core (>= 6.0.3) - Fake.Core.Context (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Environment (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.FakeVar (6.0) - Fake.Core.Context (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Process (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - System.Collections.Immutable (>= 6.0) - Fake.Core.SemVer (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.String (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Target (6.0) - Fake.Core.CommandLineParsing (>= 6.0) - Fake.Core.Context (>= 6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Control.Reactive (>= 5.0.2) - FSharp.Core (>= 6.0.3) - Fake.Core.Trace (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.IO.FileSystem (6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Tools.Git (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.SemVer (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - Fantomas.Core (6.2) - Fantomas.FCS (>= 6.2) - FSharp.Core (>= 6.0.1) - Fantomas.FCS (6.2) - FSharp.Core (>= 6.0.1) - System.Diagnostics.DiagnosticSource (>= 7.0) - System.Memory (>= 4.5.5) - System.Runtime (>= 4.3.1) - FParsec (1.1.1) - FSharp.Core (>= 4.3.4) - FSharp.Control.Reactive (5.0.5) - FSharp.Core (>= 4.7.2) - System.Reactive (>= 5.0 < 6.0) - FSharp.Core (6.0.5) - Microsoft.NETCore.Platforms (7.0.4) - Microsoft.NETCore.Targets (5.0) - System.Collections.Immutable (7.0) - System.Diagnostics.DiagnosticSource (7.0.2) - System.Memory (4.5.5) - System.Reactive (5.0) - System.Runtime (4.3.1) - Microsoft.NETCore.Platforms (>= 1.1.1) - Microsoft.NETCore.Targets (>= 1.1.3)