From 58c28874a0dd3619b7acaef75d1a5087b30d3826 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 6 Mar 2024 10:57:40 +0100 Subject: [PATCH 1/6] Detect type alias in implementation. --- .editorconfig | 20 +++- .../ParseAndCheckResults.fs | 4 + .../ParseAndCheckResults.fsi | 2 + .../CodeFixes/AddTypeAliasToSignatureFile.fs | 95 +++++++++++++++++++ .../CodeFixes/AddTypeAliasToSignatureFile.fsi | 6 ++ .../LspServers/AdaptiveServerState.fs | 3 +- .../AddTypeAliasToSignatureFileTests.fs | 20 ++++ .../CodeFixTests/Tests.fs | 3 +- 8 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs create mode 100644 src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi create mode 100644 test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs diff --git a/.editorconfig b/.editorconfig index 319a5a0c3..bf1c412cf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,4 +22,22 @@ indent_size = 2 fsharp_max_array_or_list_width=80 fsharp_max_dot_get_expression_width=80 fsharp_max_function_binding_width=80 -fsharp_max_value_binding_width=80 \ No newline at end of file +fsharp_max_value_binding_width=80 + +# Will remove after I'm done, promise! +[src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs] +indent_size = 4 +fsharp_space_before_uppercase_invocation = true +fsharp_space_before_member = true +fsharp_space_before_colon = true +fsharp_space_before_semicolon = true +fsharp_newline_between_type_definition_and_members = true +fsharp_align_function_signature_to_indentation = true +fsharp_alternative_long_member_definitions = true +fsharp_multi_line_lambda_closing_newline = true +fsharp_experimental_keep_indent_in_branch = true +fsharp_bar_before_discriminated_union_declaration = true +fsharp_keep_max_number_of_blank_lines = 1 +fsharp_experimental_elmish = true +fsharp_multiline_bracket_style = aligned +fsharp_keep_max_number_of_blank_lines = 1 diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fs b/src/FsAutoComplete.Core/ParseAndCheckResults.fs index d912e4e19..49149a55d 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -519,6 +519,10 @@ type ParseAndCheckResults let identIsland = Array.toList identIsland checkResults.GetSymbolUseAtLocation(pos.Line, colu, lineStr, identIsland) + member x.TryGetSymbolUseFromIdent (sourceText: ISourceText) (ident: Ident) : FSharpSymbolUse option = + let line = sourceText.GetLineString(ident.idRange.EndLine - 1) + x.GetCheckResults.GetSymbolUseAtLocation(ident.idRange.EndLine, ident.idRange.EndColumn, line, [ ident.idText ]) + member __.TryGetSymbolUses (pos: Position) (lineStr: LineStr) : FSharpSymbolUse list = match Lexer.findLongIdents (pos.Column, lineStr) with | None -> [] diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fsi b/src/FsAutoComplete.Core/ParseAndCheckResults.fsi index f3099f45a..524615055 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fsi +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fsi @@ -67,6 +67,8 @@ type ParseAndCheckResults = member TryGetSymbolUse: pos: Position -> lineStr: LineStr -> FSharpSymbolUse option + member TryGetSymbolUseFromIdent: ISourceText -> Ident -> FSharpSymbolUse option + member TryGetSymbolUses: pos: Position -> lineStr: LineStr -> FSharpSymbolUse list member TryGetSymbolUseAndUsages: diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs new file mode 100644 index 000000000..d19809b44 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -0,0 +1,95 @@ +module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile + +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text +open FSharp.Compiler.CodeAnalysis +open FsToolkit.ErrorHandling +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.CodeFix.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers + +// TODO: add proper title for code fix +let title = "AddTypeAliasToSignatureFile Codefix" + +let fix (getParseResultsForFile : GetParseResultsForFile) : CodeFix = + fun (codeActionParams : CodeActionParams) -> + asyncResult { + // TODO: check if the file even has a signature file + + 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 + + let typeDefnInfo = + (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) + ||> ParsedInput.tryPick (fun path node -> + match node with + | SyntaxNode.SynTypeDefn (SynTypeDefn ( + typeInfo = SynComponentInfo (longId = [ typeIdent ]) + typeRepr = SynTypeDefnRepr.Simple (simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _) + range = m)) when (Range.rangeContainsPos m fcsPos) -> + Some (typeIdent, m, path) + | _ -> None + ) + + match typeDefnInfo with + | None -> return [] + | Some (typeName, mTypeDefn, implPath) -> + + match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with + | None -> return [] + | Some typeSymbolUse -> + + match typeSymbolUse.Symbol with + | :? FSharpEntity as entity -> + let isPartOfSignature = + match entity.SignatureLocation with + | None -> false + | Some sigLocation -> Utils.isSignatureFile sigLocation.FileName + + if isPartOfSignature then + return [] + else + + 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) + + // Find a good location to insert the type alias + ignore (sigTextDocumentIdentifier, sigParseAndCheckResults, mTypeDefn, implPath) + + 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 AddTypeAliasToSignatureFile" + // } + |] + Kind = FixKind.Fix + } + ] + | _ -> return [] + } diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi new file mode 100644 index 000000000..3b00a2dde --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi @@ -0,0 +1,6 @@ +module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile + +open FsAutoComplete.CodeFix.Types + +val title: string +val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 938caa1ce..33df37017 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -1902,7 +1902,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac ToInterpolatedString.fix tryGetParseAndCheckResultsForFile getLanguageVersion AdjustConstant.fix tryGetParseAndCheckResultsForFile UpdateValueInSignatureFile.fix tryGetParseAndCheckResultsForFile - RemoveUnnecessaryParentheses.fix forceFindSourceText |]) + RemoveUnnecessaryParentheses.fix forceFindSourceText + AddTypeAliasToSignatureFile.fix tryGetParseAndCheckResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs new file mode 100644 index 000000000..ed49b4be8 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs @@ -0,0 +1,20 @@ +module private FsAutoComplete.Tests.CodeFixTests.AddTypeAliasToSignatureFileTests + +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix + +let tests state = + serverTestList (nameof AddTypeAliasToSignatureFile) state defaultConfigDto None (fun server -> + [ let selectCodeFix = CodeFix.withTitle AddTypeAliasToSignatureFile.title + + ftestCaseAsync "first unit test for AddTypeAliasToSignatureFile" + <| CodeFix.check + server + "let a$0 b c = ()" + Diagnostics.acceptAll + selectCodeFix + "let Text replaced by AddTypeAliasToSignatureFile b c = ()" + ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index df903fe4c..301ff1ed1 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -3432,4 +3432,5 @@ let tests textFactory state = removeRedundantAttributeSuffixTests state removePatternArgumentTests state UpdateValueInSignatureFileTests.tests state - removeUnnecessaryParenthesesTests state ] + removeUnnecessaryParenthesesTests state + AddTypeAliasToSignatureFileTests.tests state ] From f4f325452a2d430039b0158beaf291cb92e2aa0d Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 6 Mar 2024 15:59:55 +0100 Subject: [PATCH 2/6] Check if is file pair. --- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 184 ++++++++++-------- .../CodeFixes/AddTypeAliasToSignatureFile.fsi | 2 +- .../LspServers/AdaptiveServerState.fs | 2 +- 3 files changed, 108 insertions(+), 80 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index d19809b44..85244bf32 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -1,5 +1,6 @@ module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile +open System open FSharp.Compiler.Symbols open FSharp.Compiler.Syntax open FSharp.Compiler.Text @@ -13,83 +14,110 @@ open FsAutoComplete.LspHelpers // TODO: add proper title for code fix let title = "AddTypeAliasToSignatureFile Codefix" -let fix (getParseResultsForFile : GetParseResultsForFile) : CodeFix = - fun (codeActionParams : CodeActionParams) -> - asyncResult { - // TODO: check if the file even has a signature file - - 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 - - let typeDefnInfo = - (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) - ||> ParsedInput.tryPick (fun path node -> - match node with - | SyntaxNode.SynTypeDefn (SynTypeDefn ( - typeInfo = SynComponentInfo (longId = [ typeIdent ]) - typeRepr = SynTypeDefnRepr.Simple (simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _) - range = m)) when (Range.rangeContainsPos m fcsPos) -> - Some (typeIdent, m, path) - | _ -> None - ) - - match typeDefnInfo with - | None -> return [] - | Some (typeName, mTypeDefn, implPath) -> - - match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with - | None -> return [] - | Some typeSymbolUse -> - - match typeSymbolUse.Symbol with - | :? FSharpEntity as entity -> - let isPartOfSignature = - match entity.SignatureLocation with - | None -> false - | Some sigLocation -> Utils.isSignatureFile sigLocation.FileName - - if isPartOfSignature then - return [] - else - - 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) - - // Find a good location to insert the type alias - ignore (sigTextDocumentIdentifier, sigParseAndCheckResults, mTypeDefn, implPath) - - return - [ +let codeFixForImplementationFileWithSignature + (getProjectOptionsForFile : GetProjectOptionsForFile) + (codeFix : CodeFix) + (codeActionParams : CodeActionParams) + : Async> + = + 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) -> + 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 + + let typeDefnInfo = + (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) + ||> ParsedInput.tryPick (fun path node -> + match node with + | SyntaxNode.SynTypeDefn (SynTypeDefn ( + typeInfo = SynComponentInfo (longId = [ typeIdent ]) + typeRepr = SynTypeDefnRepr.Simple (simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _) + range = m)) when (Range.rangeContainsPos m fcsPos) -> Some (typeIdent, m, path) + | _ -> None + ) + + match typeDefnInfo with + | None -> return [] + | Some (typeName, mTypeDefn, implPath) -> + + match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with + | None -> return [] + | Some typeSymbolUse -> + + match typeSymbolUse.Symbol with + | :? FSharpEntity as entity -> + let isPartOfSignature = + match entity.SignatureLocation with + | None -> false + | Some sigLocation -> Utils.isSignatureFile sigLocation.FileName + + if isPartOfSignature then + return [] + else + + let implFilePath = codeActionParams.TextDocument.GetFilePath () + let sigFilePath = $"%s{implFilePath}i" + let sigFileName = Utils.normalizePath sigFilePath + + let sigTextDocumentIdentifier : TextDocumentIdentifier = { - 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 AddTypeAliasToSignatureFile" - // } - |] - Kind = FixKind.Fix + Uri = $"%s{codeActionParams.TextDocument.Uri}i" } - ] - | _ -> return [] - } + + let! (sigParseAndCheckResults : ParseAndCheckResults, + _sigLine : string, + _sigSourceText : IFSACSourceText) = getParseResultsForFile sigFileName (Position.mkPos 1 0) + + // Find a good location to insert the type alias + ignore (sigTextDocumentIdentifier, sigParseAndCheckResults, mTypeDefn, implPath) + + 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 AddTypeAliasToSignatureFile" + // } + |] + Kind = FixKind.Fix + } + ] + | _ -> return [] + } + ) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi index 3b00a2dde..8cd97449e 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi @@ -3,4 +3,4 @@ module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile open FsAutoComplete.CodeFix.Types val title: string -val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix +val fix: getProjectOptionsForFile: GetProjectOptionsForFile -> getParseResultsForFile: GetParseResultsForFile -> CodeFix diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 33df37017..84591cf26 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -1903,7 +1903,7 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac AdjustConstant.fix tryGetParseAndCheckResultsForFile UpdateValueInSignatureFile.fix tryGetParseAndCheckResultsForFile RemoveUnnecessaryParentheses.fix forceFindSourceText - AddTypeAliasToSignatureFile.fix tryGetParseAndCheckResultsForFile |]) + AddTypeAliasToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { From 39e6cef187d9b9b501a09a8480f40c9f38bffaf3 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 7 Mar 2024 12:04:45 +0100 Subject: [PATCH 3/6] Initial insert in signature file. --- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index 85244bf32..fd6c0812d 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -11,6 +11,8 @@ open FsAutoComplete.CodeFix.Types open FsAutoComplete open FsAutoComplete.LspHelpers +let mkLongIdRange (lid : LongIdent) = lid |> List.map (fun ident -> ident.idRange) |> List.reduce Range.unionRanges + // TODO: add proper title for code fix let title = "AddTypeAliasToSignatureFile Codefix" @@ -55,18 +57,21 @@ let fix let typeDefnInfo = (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) - ||> ParsedInput.tryPick (fun path node -> + ||> ParsedInput.tryPick (fun _path node -> match node with | SyntaxNode.SynTypeDefn (SynTypeDefn ( typeInfo = SynComponentInfo (longId = [ typeIdent ]) typeRepr = SynTypeDefnRepr.Simple (simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _) - range = m)) when (Range.rangeContainsPos m fcsPos) -> Some (typeIdent, m, path) + range = m + trivia = trivia)) when (Range.rangeContainsPos m fcsPos) -> + let mFull = Range.unionRanges trivia.LeadingKeyword.Range m + Some (typeIdent, mFull) | _ -> None ) match typeDefnInfo with | None -> return [] - | Some (typeName, mTypeDefn, implPath) -> + | Some (typeName, mTypeDefn) -> match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with | None -> return [] @@ -96,24 +101,64 @@ let fix _sigLine : string, _sigSourceText : IFSACSourceText) = getParseResultsForFile sigFileName (Position.mkPos 1 0) + let parentSigLocation = + entity.DeclaringEntity + |> Option.bind (fun parentEntity -> + match parentEntity.SignatureLocation with + | Some sigLocation when Utils.isSignatureFile sigLocation.FileName -> Some sigLocation + | _ -> None + ) + + match parentSigLocation with + | None -> return [] + | Some parentSigLocation -> + // Find a good location to insert the type alias - ignore (sigTextDocumentIdentifier, sigParseAndCheckResults, mTypeDefn, implPath) + let mInsert = + (parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynModuleOrNamespaceSig (SynModuleOrNamespaceSig ( + longId = longId ; decls = decls)) + | SyntaxNode.SynModuleSigDecl (SynModuleSigDecl.NestedModule ( + moduleInfo = SynComponentInfo (longId = longId) ; 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 + + decls + // Skip open statements + |> List.tryFind ( + function + | SynModuleSigDecl.Open _ -> false + | _ -> true + ) + |> Option.map (fun mdl -> mdl.Range.StartRange) + + | _ -> None + ) + + match mInsert with + | None -> return [] + | Some mInsert -> + + let newText = String.Concat (sourceText.GetSubTextFromRange mTypeDefn, "\n\n") return [ { SourceDiagnostic = None Title = title - File = codeActionParams.TextDocument - // Based on conditional logic, you typically want to suggest a text edit to the user. + File = sigTextDocumentIdentifier 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 AddTypeAliasToSignatureFile" - // } + { + Range = fcsRangeToLsp mInsert + NewText = newText + } |] Kind = FixKind.Fix } From c3405a113549f16164471ee392ac8f6a042dfe13 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 7 Mar 2024 13:15:48 +0100 Subject: [PATCH 4/6] Add first unit test --- .editorconfig | 17 +++++++ .../CodeFixes/AddTypeAliasToSignatureFile.fs | 40 ++++++++++++--- .../AddTypeAliasToSignatureFileTests.fs | 51 +++++++++++++++---- .../UpdateValueInSignatureFileTests.fs | 43 +++------------- .../Utils/CursorbasedTests.fs | 32 +++++++++++- .../Utils/CursorbasedTests.fsi | 10 ++++ 6 files changed, 135 insertions(+), 58 deletions(-) diff --git a/.editorconfig b/.editorconfig index bf1c412cf..91ffd2181 100644 --- a/.editorconfig +++ b/.editorconfig @@ -41,3 +41,20 @@ fsharp_keep_max_number_of_blank_lines = 1 fsharp_experimental_elmish = true fsharp_multiline_bracket_style = aligned fsharp_keep_max_number_of_blank_lines = 1 + +[test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs] +indent_size = 4 +fsharp_space_before_uppercase_invocation = true +fsharp_space_before_member = true +fsharp_space_before_colon = true +fsharp_space_before_semicolon = true +fsharp_newline_between_type_definition_and_members = true +fsharp_align_function_signature_to_indentation = true +fsharp_alternative_long_member_definitions = true +fsharp_multi_line_lambda_closing_newline = true +fsharp_experimental_keep_indent_in_branch = true +fsharp_bar_before_discriminated_union_declaration = true +fsharp_keep_max_number_of_blank_lines = 1 +fsharp_experimental_elmish = true +fsharp_multiline_bracket_style = aligned +fsharp_keep_max_number_of_blank_lines = 1 diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index fd6c0812d..b4882c0b3 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -13,8 +13,27 @@ open FsAutoComplete.LspHelpers let mkLongIdRange (lid : LongIdent) = lid |> List.map (fun ident -> ident.idRange) |> List.reduce Range.unionRanges +let (|AllOpenOrHashDirective|_|) (decls : SynModuleSigDecl list) : range option = + match decls with + | [] -> None + | decls -> + + let allOpenOrHashDirective = + decls + |> List.forall ( + function + | SynModuleSigDecl.Open _ + | SynModuleSigDecl.HashDirective _ -> true + | _ -> false + ) + + if not allOpenOrHashDirective then + None + else + Some (List.last decls).Range.EndRange + // TODO: add proper title for code fix -let title = "AddTypeAliasToSignatureFile Codefix" +let title = "Add type alias to signature file" let codeFixForImplementationFileWithSignature (getProjectOptionsForFile : GetProjectOptionsForFile) @@ -114,7 +133,7 @@ let fix | Some parentSigLocation -> // Find a good location to insert the type alias - let mInsert = + let insertText = (parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree) ||> ParsedInput.tryPick (fun _path node -> match node with @@ -129,6 +148,14 @@ let fix None else + let aliasText = sourceText.GetSubTextFromRange mTypeDefn + + match decls with + | [] -> failwith "todo: empty module" + | AllOpenOrHashDirective mLastDecl -> + Some (mLastDecl, String.Concat ("\n\n", sourceText.GetSubTextFromRange mTypeDefn)) + | decls -> + decls // Skip open statements |> List.tryFind ( @@ -136,16 +163,13 @@ let fix | SynModuleSigDecl.Open _ -> false | _ -> true ) - |> Option.map (fun mdl -> mdl.Range.StartRange) - + |> Option.map (fun mdl -> mdl.Range.StartRange, String.Concat (aliasText, "\n\n")) | _ -> None ) - match mInsert with + match insertText with | None -> return [] - | Some mInsert -> - - let newText = String.Concat (sourceText.GetSubTextFromRange mTypeDefn, "\n\n") + | Some (mInsert, newText) -> return [ diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs index ed49b4be8..87c6b05cb 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs @@ -1,20 +1,49 @@ module private FsAutoComplete.Tests.CodeFixTests.AddTypeAliasToSignatureFileTests +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 AddTypeAliasToSignatureFile) state defaultConfigDto None (fun server -> - [ let selectCodeFix = CodeFix.withTitle AddTypeAliasToSignatureFile.title - - ftestCaseAsync "first unit test for AddTypeAliasToSignatureFile" - <| CodeFix.check - server - "let a$0 b c = ()" - Diagnostics.acceptAll - selectCodeFix - "let Text replaced by AddTypeAliasToSignatureFile b c = ()" - ]) + serverTestList + (nameof AddTypeAliasToSignatureFile) + state + defaultConfigDto + (Some path) + (fun server -> + [ + let selectCodeFix = CodeFix.withTitle AddTypeAliasToSignatureFile.title + + ftestCaseAsync + "Sample use-case for AddTypeAliasToSignatureFile" + (CodeFix.checkCodeFixInImplementationAndVerifySignature + server + """ +module Foo + +open Foo +""" + """ +module Foo + +open Foo + +type Bar = $0int +""" + Diagnostics.acceptAll + selectCodeFix + """ +module Foo + +open Foo + +type Bar = int +""") + ] + ) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs index 4b3a19273..82ffb6979 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/UpdateValueInSignatureFileTests.fs @@ -6,63 +6,32 @@ open Helpers open Utils.ServerTests open Utils.CursorbasedTests open FsAutoComplete.CodeFix -open Utils.Utils -open Utils.TextEdit -open Utils.Server open Utils.CursorbasedTests.CodeFix -let path = Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") -let fsiFile, fsFile = ("Code.fsi", "Code.fs") - -let checkWithFsi - server - fsiSource - fsSourceWithCursor - selectCodeFix - fsiSourceExpected - = async { - let fsiSource = fsiSource |> Text.trimTripleQuotation - let cursor, fsSource = - fsSourceWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange - let! fsiDoc, diags = server |> Server.openDocumentWithText fsiFile fsiSource - use fsiDoc = fsiDoc - Expect.isEmpty diags "There should be no diagnostics in fsi doc" - let! fsDoc, diags = server |> Server.openDocumentWithText fsFile fsSource - use fsDoc = fsDoc - - do! - checkFixAt - (fsDoc, diags) - fsiDoc.VersionedTextDocumentIdentifier - (fsiSource, cursor) - (Diagnostics.expectCode "34") - selectCodeFix - (After (fsiSourceExpected |> Text.trimTripleQuotation)) - } +let path = + Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") let tests state = serverTestList (nameof UpdateValueInSignatureFile) state defaultConfigDto (Some path) (fun server -> [ let selectCodeFix = CodeFix.withTitle UpdateValueInSignatureFile.title testCaseAsync "first unit test for UpdateValueInSignatureFile" - <| checkWithFsi + <| checkCodeFixInImplementationAndVerifySignature server """ module A val a: b:int -> int """ -""" + """ module A let a$0 (b:int) (c: string) = 0 """ + (Diagnostics.expectCode "34") selectCodeFix """ module A val a: b: int -> c: string -> int -""" - ]) +""" ]) diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs index 00fa8725f..f67bc61fc 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fs @@ -56,8 +56,6 @@ module CodeFix = | Some(Helpers.CodeActions actions), _ -> actions | Some _, _ -> failwith "Expected some code actions from the server" - - // select code action to use let codeActions = chooseFix allCodeActions @@ -180,6 +178,36 @@ module CodeFix = let withTitle title = matching (fun f -> f.Title = title) let ofKind kind = matching (fun f -> f.Kind = Some kind) + let checkCodeFixInImplementationAndVerifySignature + (server: CachedServer) + (fsiSource: string) + (fsSourceWithCursor: string) + (validateDiagnostics: Diagnostic[] -> unit) + (selectCodeFix: ChooseFix) + (fsiSourceExpected: string) + : Async = + async { + let fsiFile, fsFile = ("Code.fsi", "Code.fs") + let fsiSource = fsiSource |> Text.trimTripleQuotation + + let cursor, fsSource = + fsSourceWithCursor |> Text.trimTripleQuotation |> Cursor.assertExtractRange + + let! fsiDoc, _diags = server |> Server.openDocumentWithText fsiFile fsiSource + use fsiDoc = fsiDoc + let! fsDoc, diags = server |> Server.openDocumentWithText fsFile fsSource + use fsDoc = fsDoc + + do! + checkFixAt + (fsDoc, diags) + fsiDoc.VersionedTextDocumentIdentifier + (fsiSource, cursor) + validateDiagnostics + selectCodeFix + (After(fsiSourceExpected |> Text.trimTripleQuotation)) + } + /// Bundled tests in Expecto test module private Test = /// One `testCaseAsync` for each cursorRange. diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi index 0812f801e..7eeb78398 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi +++ b/test/FsAutoComplete.Tests.Lsp/Utils/CursorbasedTests.fsi @@ -89,6 +89,16 @@ module CodeFix = val withTitle: title: string -> (CodeAction array -> CodeAction array) val ofKind: kind: string -> (CodeAction array -> CodeAction array) + /// Execute a codefix in an implementation file and assert the resulting change in a signature file. + val checkCodeFixInImplementationAndVerifySignature: + server: CachedServer -> + fsiSource: string -> + fsSourceWithCursor: string -> + validateDiagnostics: (Diagnostic array -> unit) -> + selectCodeFix: ChooseFix -> + fsiSourceExpected: string -> + Async + /// Bundled tests in Expecto test module private Test = /// One `testCaseAsync` for each cursorRange. From 4320da50e5133fd9375afecf616ec94dcf613a8f Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 7 Mar 2024 16:21:44 +0100 Subject: [PATCH 5/6] Format code --- .editorconfig | 35 -- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 339 +++++++++--------- .../AddTypeAliasToSignatureFileTests.fs | 187 ++++++++-- 3 files changed, 335 insertions(+), 226 deletions(-) diff --git a/.editorconfig b/.editorconfig index 91ffd2181..8d1e1bb03 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,38 +23,3 @@ fsharp_max_array_or_list_width=80 fsharp_max_dot_get_expression_width=80 fsharp_max_function_binding_width=80 fsharp_max_value_binding_width=80 - -# Will remove after I'm done, promise! -[src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs] -indent_size = 4 -fsharp_space_before_uppercase_invocation = true -fsharp_space_before_member = true -fsharp_space_before_colon = true -fsharp_space_before_semicolon = true -fsharp_newline_between_type_definition_and_members = true -fsharp_align_function_signature_to_indentation = true -fsharp_alternative_long_member_definitions = true -fsharp_multi_line_lambda_closing_newline = true -fsharp_experimental_keep_indent_in_branch = true -fsharp_bar_before_discriminated_union_declaration = true -fsharp_keep_max_number_of_blank_lines = 1 -fsharp_experimental_elmish = true -fsharp_multiline_bracket_style = aligned -fsharp_keep_max_number_of_blank_lines = 1 - -[test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs] -indent_size = 4 -fsharp_space_before_uppercase_invocation = true -fsharp_space_before_member = true -fsharp_space_before_colon = true -fsharp_space_before_semicolon = true -fsharp_newline_between_type_definition_and_members = true -fsharp_align_function_signature_to_indentation = true -fsharp_alternative_long_member_definitions = true -fsharp_multi_line_lambda_closing_newline = true -fsharp_experimental_keep_indent_in_branch = true -fsharp_bar_before_discriminated_union_declaration = true -fsharp_keep_max_number_of_blank_lines = 1 -fsharp_experimental_elmish = true -fsharp_multiline_bracket_style = aligned -fsharp_keep_max_number_of_blank_lines = 1 diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index b4882c0b3..88b8abe5d 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -11,182 +11,189 @@ open FsAutoComplete.CodeFix.Types open FsAutoComplete open FsAutoComplete.LspHelpers -let mkLongIdRange (lid : LongIdent) = lid |> List.map (fun ident -> ident.idRange) |> List.reduce Range.unionRanges +let mkLongIdRange (lid: LongIdent) = lid |> List.map (fun ident -> ident.idRange) |> List.reduce Range.unionRanges -let (|AllOpenOrHashDirective|_|) (decls : SynModuleSigDecl list) : range option = - match decls with - | [] -> None - | decls -> +let (|AllOpenOrHashDirective|_|) (decls: SynModuleSigDecl list) : range option = + match decls with + | [] -> None + | decls -> let allOpenOrHashDirective = - decls - |> List.forall ( - function - | SynModuleSigDecl.Open _ - | SynModuleSigDecl.HashDirective _ -> true - | _ -> false - ) + decls + |> List.forall (function + | SynModuleSigDecl.Open _ + | SynModuleSigDecl.HashDirective _ -> true + | _ -> false) if not allOpenOrHashDirective then - None + None else - Some (List.last decls).Range.EndRange + Some (List.last decls).Range.EndRange + +type SynTypeDefn with + + member x.FullRange = + match x with + | SynTypeDefn(range = m; trivia = { LeadingKeyword = lk }) -> Range.unionRanges lk.Range m -// TODO: add proper title for code fix let title = "Add type alias to signature file" let codeFixForImplementationFileWithSignature - (getProjectOptionsForFile : GetProjectOptionsForFile) - (codeFix : CodeFix) - (codeActionParams : CodeActionParams) - : Async> - = - 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 - } + (getProjectOptionsForFile: GetProjectOptionsForFile) + (codeFix: CodeFix) + (codeActionParams: CodeActionParams) + : Async> = + 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) -> - 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 - - let typeDefnInfo = - (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) - ||> ParsedInput.tryPick (fun _path node -> - match node with - | SyntaxNode.SynTypeDefn (SynTypeDefn ( - typeInfo = SynComponentInfo (longId = [ typeIdent ]) - typeRepr = SynTypeDefnRepr.Simple (simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _) - range = m - trivia = trivia)) when (Range.rangeContainsPos m fcsPos) -> - let mFull = Range.unionRanges trivia.LeadingKeyword.Range m - Some (typeIdent, mFull) - | _ -> None - ) - - match typeDefnInfo with - | None -> return [] - | Some (typeName, mTypeDefn) -> - - match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with + (getProjectOptionsForFile: GetProjectOptionsForFile) + (getParseResultsForFile: GetParseResultsForFile) + : CodeFix = + codeFixForImplementationFileWithSignature 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 + + let typeDefnInfo = + (fcsPos, parseAndCheckResults.GetParseResults.ParseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynTypeDefn(SynTypeDefn( + typeInfo = SynComponentInfo(longId = [ typeIdent ]) + typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _) + trivia = trivia) as tdn) when (Range.rangeContainsPos tdn.FullRange fcsPos) -> + let mFull = Range.unionRanges trivia.LeadingKeyword.Range tdn.FullRange + Some(typeIdent, mFull) + | _ -> None) + + match typeDefnInfo with + | None -> return [] + | Some(typeName, mTypeDefn) -> + + match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with + | None -> return [] + | Some typeSymbolUse -> + + match typeSymbolUse.Symbol with + | :? FSharpEntity as entity -> + let isPartOfSignature = + match entity.SignatureLocation with + | None -> false + | Some sigLocation -> Utils.isSignatureFile sigLocation.FileName + + if isPartOfSignature then + return [] + else + + 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 parentSigLocation = + entity.DeclaringEntity + |> Option.bind (fun parentEntity -> + match parentEntity.SignatureLocation with + | Some sigLocation when Utils.isSignatureFile sigLocation.FileName -> Some sigLocation + | _ -> None) + + match parentSigLocation with + | None -> return [] + | Some parentSigLocation -> + + // Find a good location to insert the type alias + let insertText = + (parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree) + ||> ParsedInput.tryPick (fun _path node -> + match node with + | SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(longId = longId; decls = decls)) + | SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule( + moduleInfo = SynComponentInfo(longId = longId); 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 + + let aliasText = + let text = sourceText.GetSubTextFromRange mTypeDefn + + if not (text.StartsWith("and", StringComparison.Ordinal)) then + text + else + String.Concat("type", text.Substring 3) + + match decls with + | [] -> + match node with + | SyntaxNode.SynModuleOrNamespaceSig nm -> + Some(nm.Range.EndRange, String.Concat("\n\n", aliasText)) + + | SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule( + range = mNested + trivia = { ModuleKeyword = Some mModule + EqualsRange = Some mEquals })) -> + let moduleEqualsText = + sigSourceText.GetSubTextFromRange(Range.unionRanges mModule mEquals) + // Can this grabbed from configuration? + let indent = " " + + Some(mNested, String.Concat(moduleEqualsText, "\n", indent, aliasText)) + | _ -> None + | AllOpenOrHashDirective mLastDecl -> Some(mLastDecl, String.Concat("\n\n", aliasText)) + | decls -> + + decls + // Skip open statements + |> List.tryFind (function + | SynModuleSigDecl.Open _ -> false + | _ -> true) + |> Option.map (fun mdl -> + let offset = + if mdl.Range.StartColumn = 0 then + String.Empty + else + String.replicate mdl.Range.StartColumn " " + + mdl.Range.StartRange, String.Concat(aliasText, "\n\n", offset)) + | _ -> None) + + match insertText with | None -> return [] - | Some typeSymbolUse -> - - match typeSymbolUse.Symbol with - | :? FSharpEntity as entity -> - let isPartOfSignature = - match entity.SignatureLocation with - | None -> false - | Some sigLocation -> Utils.isSignatureFile sigLocation.FileName - - if isPartOfSignature then - return [] - else - - 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 parentSigLocation = - entity.DeclaringEntity - |> Option.bind (fun parentEntity -> - match parentEntity.SignatureLocation with - | Some sigLocation when Utils.isSignatureFile sigLocation.FileName -> Some sigLocation - | _ -> None - ) - - match parentSigLocation with - | None -> return [] - | Some parentSigLocation -> - - // Find a good location to insert the type alias - let insertText = - (parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree) - ||> ParsedInput.tryPick (fun _path node -> - match node with - | SyntaxNode.SynModuleOrNamespaceSig (SynModuleOrNamespaceSig ( - longId = longId ; decls = decls)) - | SyntaxNode.SynModuleSigDecl (SynModuleSigDecl.NestedModule ( - moduleInfo = SynComponentInfo (longId = longId) ; 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 - - let aliasText = sourceText.GetSubTextFromRange mTypeDefn - - match decls with - | [] -> failwith "todo: empty module" - | AllOpenOrHashDirective mLastDecl -> - Some (mLastDecl, String.Concat ("\n\n", sourceText.GetSubTextFromRange mTypeDefn)) - | decls -> - - decls - // Skip open statements - |> List.tryFind ( - function - | SynModuleSigDecl.Open _ -> false - | _ -> true - ) - |> Option.map (fun mdl -> mdl.Range.StartRange, String.Concat (aliasText, "\n\n")) - | _ -> None - ) - - match insertText with - | None -> return [] - | Some (mInsert, newText) -> - - return - [ - { - SourceDiagnostic = None - Title = title - File = sigTextDocumentIdentifier - Edits = - [| - { - Range = fcsRangeToLsp mInsert - NewText = newText - } - |] - Kind = FixKind.Fix - } - ] - | _ -> return [] - } - ) + | Some(mInsert, newText) -> + + return + [ { SourceDiagnostic = None + Title = title + File = sigTextDocumentIdentifier + Edits = + [| { Range = fcsRangeToLsp mInsert + NewText = newText } |] + Kind = FixKind.Fix } ] + | _ -> return [] + }) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs index 87c6b05cb..78ac766d2 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddTypeAliasToSignatureFileTests.fs @@ -8,42 +8,179 @@ open Utils.CursorbasedTests open FsAutoComplete.CodeFix let path = - Path.Combine (__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") + Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") let tests state = - serverTestList - (nameof AddTypeAliasToSignatureFile) - state - defaultConfigDto - (Some path) - (fun server -> - [ - let selectCodeFix = CodeFix.withTitle AddTypeAliasToSignatureFile.title - - ftestCaseAsync - "Sample use-case for AddTypeAliasToSignatureFile" - (CodeFix.checkCodeFixInImplementationAndVerifySignature - server - """ + serverTestList (nameof AddTypeAliasToSignatureFile) state defaultConfigDto (Some path) (fun server -> + let selectCodeFix = CodeFix.withTitle AddTypeAliasToSignatureFile.title + + let test name sigBefore impl sigAfter = + testCaseAsync + name + (CodeFix.checkCodeFixInImplementationAndVerifySignature + server + sigBefore + impl + Diagnostics.acceptAll + selectCodeFix + sigAfter) + + [ test + "Sample use-case for AddTypeAliasToSignatureFile" + """ module Foo -open Foo +open System """ - """ + """ module Foo -open Foo +open System type Bar = $0int """ - Diagnostics.acceptAll - selectCodeFix - """ + """ module Foo -open Foo +open System type Bar = int -""") - ] - ) +""" + + test + "place type alias above existing val" + """ +module Foo + +open System + +val a: int +""" + """ +module Foo + +open System + +let a = 8 + +type Bar$0 = string +""" + """ +module Foo + +open System + +type Bar = string + +val a: int +""" + + test + "place type alias above existing type" + """ +namespace Foo + +[] +type A = + new: unit -> A +""" + """ +namespace Foo + +type A() = class end + +type $0P = int -> int +""" + """ +namespace Foo + +type P = int -> int + +[] +type A = + new: unit -> A +""" + + test + "replace and with type keyword" + """ +namespace Foo + +open System +""" + """ +namespace Foo + +open System + +type Foo = class end +and Bar$0 = string +""" + """ +namespace Foo + +open System + +type Bar = string +""" + + test + "empty module" + """ +namespace Foo +""" + """ +namespace Foo + +type $0Foo = int +""" + """ +namespace Foo + +type Foo = int +""" + + test + "empty nested module" + """ +namespace Foo + +module Bar = + begin end +""" + """ +namespace Foo + +module Bar = + type $0Foo = int +""" + """ +namespace Foo + +module Bar = + type Foo = int +""" + + test + "non empty nested module" + """ +namespace Foo + +module Bar = + val a: int +""" + """ +namespace Foo + +module Bar = + let a = -9 + type $0Foo = int +""" + """ +namespace Foo + +module Bar = + type Foo = int + + val a: int +""" ]) From 219cddebac0faee44cec320af107b042b52bf350 Mon Sep 17 00:00:00 2001 From: nojaf Date: Thu, 7 Mar 2024 16:56:53 +0100 Subject: [PATCH 6/6] Clean up --- .../CodeFixes/AddTypeAliasToSignatureFile.fs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs index 88b8abe5d..539f6a68d 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs @@ -79,10 +79,10 @@ let fix match node with | SyntaxNode.SynTypeDefn(SynTypeDefn( typeInfo = SynComponentInfo(longId = [ typeIdent ]) - typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _) - trivia = trivia) as tdn) when (Range.rangeContainsPos tdn.FullRange fcsPos) -> - let mFull = Range.unionRanges trivia.LeadingKeyword.Range tdn.FullRange - Some(typeIdent, mFull) + typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _)) as tdn) when + (Range.rangeContainsPos tdn.FullRange fcsPos) + -> + Some(typeIdent, tdn.FullRange) | _ -> None) match typeDefnInfo with @@ -171,7 +171,8 @@ let fix decls // Skip open statements |> List.tryFind (function - | SynModuleSigDecl.Open _ -> false + | SynModuleSigDecl.Open _ + | SynModuleSigDecl.HashDirective _ -> false | _ -> true) |> Option.map (fun mdl -> let offset =