Skip to content

Commit

Permalink
Add binding to signature file code fix. (#1249)
Browse files Browse the repository at this point in the history
* Initial Code fix.

* Initial test

* Add tests
  • Loading branch information
nojaf authored Mar 19, 2024
1 parent 76404e4 commit e505c1f
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 26 deletions.
22 changes: 22 additions & 0 deletions src/FsAutoComplete/CodeFixes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,25 @@ module Run =

let ifDiagnosticByCode codes handler : CodeFix =
runDiagnostics (fun d -> d.Code.IsSome && Set.contains d.Code.Value codes) handler

let ifImplementationFileBackedBySignature
(getProjectOptionsForFile: GetProjectOptionsForFile)
(codeFix: CodeFix)
(codeActionParams: CodeActionParams)
: Async<Result<Fix list, string>> =
async {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let! project = getProjectOptionsForFile fileName

match project with
| Error _ -> return Ok []
| Ok projectOptions ->

let signatureFile = System.String.Concat(fileName, "i")
let hasSig = projectOptions.SourceFiles |> Array.contains signatureFile

if not hasSig then
return Ok []
else
return! codeFix codeActionParams
}
6 changes: 6 additions & 0 deletions src/FsAutoComplete/CodeFixes.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,9 @@ module Run =
codes: Set<string> ->
handler: (Diagnostic -> CodeActionParams -> Async<Result<Fix list, string>>) ->
(CodeActionParams -> Async<Result<Fix list, string>>)

val ifImplementationFileBackedBySignature:
getProjectOptionsForFile: GetProjectOptionsForFile ->
codeFix: CodeFix ->
codeActionParams: CodeActionParams ->
Async<Result<Fix list, string>>
149 changes: 149 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddBindingToSignatureFile.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
module FsAutoComplete.CodeFix.AddBindingToSignatureFile

open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FsToolkit.ErrorHandling
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers
open FsAutoComplete.Patterns.SymbolUse

let title = "Add binding to signature file"

let (|IdentifierFromHeadPat|_|) (pat: SynPat) =
match pat with
| SynPat.LongIdent(longDotId = SynLongIdent(id = [ nameIdent ]))
| SynPat.Named(ident = SynIdent(ident = nameIdent)) -> Some nameIdent
| _ -> None

let (|SignatureValText|_|) (displayContext: FSharpDisplayContext) (symbolUse: FSharpSymbolUse) =
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv -> mfv.GetValSignatureText(displayContext, symbolUse.Range)
| _ -> None

let mkLongIdRange (lid: LongIdent) = lid |> List.map (fun ident -> ident.idRange) |> List.reduce Range.unionRanges

[<RequireQualifiedAccess>]
type InsertLocation =
/// Could be parent node or last sibling
| AfterNode of columnOffset: int * endRange: range
| ReplaceEmptyNestedModule of moduleKeywordStart: int * afterEqualsTillEnd: range

let fix
(getProjectOptionsForFile: GetProjectOptionsForFile)
(getParseResultsForFile: GetParseResultsForFile)
: CodeFix =
Run.ifImplementationFileBackedBySignature getProjectOptionsForFile (fun (codeActionParams: CodeActionParams) ->
asyncResult {
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

// Find a top level binding ident
let topLevelBindingIdentName =
(fcsPos, parseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun path node ->
match List.tryHead path, node with
| Some(SyntaxNode.SynModule(SynModuleDecl.Let _)),
SyntaxNode.SynBinding(SynBinding(
headPat = IdentifierFromHeadPat nameIdent
trivia = { LeadingKeyword = lk
EqualsRange = Some mEq })) ->
let mLetTillEquals = Range.unionRanges lk.Range mEq

if Range.rangeContainsPos mLetTillEquals fcsPos then
Some nameIdent
else
None
| _ -> None)

match topLevelBindingIdentName with
| None -> return []
| Some identName ->

// Check if the parent of its symbol exists in the signature file.
match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText identName with
| Some(IsParentInSignature parentSigLocation as bindingSymbolUse) ->

let implFilePath = codeActionParams.TextDocument.GetFilePath()
let sigFilePath = $"%s{implFilePath}i"
let sigFileName = Utils.normalizePath sigFilePath

let sigTextDocumentIdentifier: TextDocumentIdentifier =
{ Uri = $"%s{codeActionParams.TextDocument.Uri}i" }

let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, sigSourceText: IFSACSourceText) =
getParseResultsForFile sigFileName (Position.mkPos 1 0)

let sigParentIdent =
let text = sigSourceText.GetSubTextFromRange parentSigLocation
FSharp.Compiler.Syntax.Ident(text, parentSigLocation)

match sigParseAndCheckResults.TryGetSymbolUseFromIdent sigSourceText sigParentIdent with
| None -> return []
| Some parentSigSymbolUse ->
// Get the val text (using the DisplayContext from the parent in the signature file).
match bindingSymbolUse with
| SignatureValText parentSigSymbolUse.DisplayContext valText ->
// Find the end of the parent (in the signature file)
let insertLocation: InsertLocation option =
(parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
match node with
| SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(
longId = longId; range = mParent; decls = decls))
| SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(
moduleInfo = SynComponentInfo(longId = longId); range = mParent; moduleDecls = decls)) ->
let mSigName = mkLongIdRange longId

// `parentSigLocation` will only contain the single identifier in case a module is prefixed with a namespace.
if not (Range.rangeContainsRange mSigName parentSigLocation) then
None
else
// Use the last decl to get the indentation right in case of a nested module.
match List.tryLast decls with
| None ->
match node with
| SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(
trivia = { ModuleKeyword = Some mk
EqualsRange = Some mEq }
range = mFull)) ->
let mAfterEqualsTillEnd = Range.unionRanges mEq.EndRange mFull.EndRange
Some(InsertLocation.ReplaceEmptyNestedModule(mk.StartColumn, mAfterEqualsTillEnd))
| _ -> Some(InsertLocation.AfterNode(mParent.StartColumn, mParent.EndRange))
| Some lastDecl ->
Some(InsertLocation.AfterNode(lastDecl.Range.StartColumn, lastDecl.Range.EndRange))

| _ -> None)

match insertLocation with
| None -> return []
| Some insertLocation ->

let newText, m =
match insertLocation with
| InsertLocation.AfterNode(columnOffset, endRange) ->
let indent = String.replicate columnOffset " "
$"\n\n%s{indent}{valText}", endRange
| InsertLocation.ReplaceEmptyNestedModule(columnOffset, mReplace) ->
// TODO: can we get the indent_size from configuration??
let indent = String.replicate (columnOffset + 4) " "
$"\n%s{indent}%s{valText}", mReplace

return
[ { SourceDiagnostic = None
Title = title
File = sigTextDocumentIdentifier
Edits =
[| { Range = fcsRangeToLsp m
NewText = newText } |]
Kind = FixKind.Fix } ]
| _ -> return []
| _ -> return []
})
6 changes: 6 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddBindingToSignatureFile.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module FsAutoComplete.CodeFix.AddBindingToSignatureFile

open FsAutoComplete.CodeFix.Types

val title: string
val fix: getProjectOptionsForFile: GetProjectOptionsForFile -> getParseResultsForFile: GetParseResultsForFile -> CodeFix
25 changes: 1 addition & 24 deletions src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile
open System
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharp.Compiler.CodeAnalysis
open FsToolkit.ErrorHandling
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
Expand Down Expand Up @@ -38,33 +37,11 @@ type SynTypeDefn with

let title = "Add type alias to signature file"

let codeFixForImplementationFileWithSignature
(getProjectOptionsForFile: GetProjectOptionsForFile)
(codeFix: CodeFix)
(codeActionParams: CodeActionParams)
: Async<Result<Fix list, string>> =
async {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let! project = getProjectOptionsForFile fileName

match project with
| Error _ -> return Ok []
| Ok projectOptions ->

let signatureFile = String.Concat(fileName, "i")
let hasSig = projectOptions.SourceFiles |> Array.contains signatureFile

if not hasSig then
return Ok []
else
return! codeFix codeActionParams
}

let fix
(getProjectOptionsForFile: GetProjectOptionsForFile)
(getParseResultsForFile: GetParseResultsForFile)
: CodeFix =
codeFixForImplementationFileWithSignature getProjectOptionsForFile (fun (codeActionParams: CodeActionParams) ->
Run.ifImplementationFileBackedBySignature getProjectOptionsForFile (fun (codeActionParams: CodeActionParams) ->
asyncResult {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
// The converted LSP start position to an FCS start position.
Expand Down
3 changes: 2 additions & 1 deletion src/FsAutoComplete/LspServers/AdaptiveServerState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1923,7 +1923,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac
UpdateValueInSignatureFile.fix tryGetParseAndCheckResultsForFile
RemoveUnnecessaryParentheses.fix forceFindSourceText
AddTypeAliasToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile
UpdateTypeAbbreviationInSignatureFile.fix tryGetParseAndCheckResultsForFile |])
UpdateTypeAbbreviationInSignatureFile.fix tryGetParseAndCheckResultsForFile
AddBindingToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile |])

let forgetDocument (uri: DocumentUri) =
async {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
module private FsAutoComplete.Tests.CodeFixTests.AddBindingToSignatureFileTests

open System.IO
open Expecto
open Helpers
open Utils.ServerTests
open Utils.CursorbasedTests
open FsAutoComplete.CodeFix

let path =
Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/")

let tests state =
serverTestList (nameof AddBindingToSignatureFile) state defaultConfigDto (Some path) (fun server ->
let selectCodeFix = CodeFix.withTitle AddBindingToSignatureFile.title

let test name sigBefore impl sigAfter =
testCaseAsync
name
(CodeFix.checkCodeFixInImplementationAndVerifySignature
server
sigBefore
impl
Diagnostics.acceptAll
selectCodeFix
sigAfter)

[

test
"Add simple function binding"
"""
module Foo
"""
"""
module Foo
let a$0 b = b - 1
"""
"""
module Foo
val a: b: int -> int
"""

test
"Add value binding"
"""
module Foo
"""
"""
module Foo
let$0 a = 'c'
"""
"""
module Foo
val a: char
"""

test
"Add function binding using display context of signature file"
"""
module Foo
"""
"""
module Foo
open System
let d$0 (v:DateTime) = v
"""
"""
module Foo
val d: v: System.DateTime -> System.DateTime
"""

test
"Add binding to nested module"
"""
namespace Foo
module Bar =
val x: int
"""
"""
namespace Foo
module Bar =
let x = 42
let a$0 b = b - 1
"""
"""
namespace Foo
module Bar =
val x: int
val a: b: int -> int
"""

test
"Add binding to empty nested module"
"""
namespace Foo
module Bar = begin end
"""
"""
namespace Foo
module Bar =
let a$0 b = b - 1
"""
"""
namespace Foo
module Bar =
val a: b: int -> int
""" ])
3 changes: 2 additions & 1 deletion test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3434,4 +3434,5 @@ let tests textFactory state =
UpdateValueInSignatureFileTests.tests state
removeUnnecessaryParenthesesTests state
AddTypeAliasToSignatureFileTests.tests state
UpdateTypeAbbreviationInSignatureFileTests.tests state ]
UpdateTypeAbbreviationInSignatureFileTests.tests state
AddBindingToSignatureFileTests.tests state ]

0 comments on commit e505c1f

Please sign in to comment.