diff --git a/src/FsAutoComplete/CodeFixes/ReplaceLambdaWithDotLambda.fs b/src/FsAutoComplete/CodeFixes/ReplaceLambdaWithDotLambda.fs new file mode 100644 index 000000000..cab6e5e4d --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ReplaceLambdaWithDotLambda.fs @@ -0,0 +1,95 @@ +module FsAutoComplete.CodeFix.ReplaceLambdaWithDotLambda + +open FSharp.Compiler.Syntax +open FsAutoComplete.FCSPatches +open FsToolkit.ErrorHandling +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.CodeFix.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers +open FSharp.Compiler.Text.Range +open FSharp.Compiler.Text + +[] +let (|LongIdentExprIdentifier|_|) = + function + | SynExpr.LongIdent(longDotId = SynLongIdent(id = identifierIdent :: _)) -> ValueSome identifierIdent + | _ -> ValueNone + +[] +type ReplaceInfo = + | RawLambda of range + | ParenLambda of lpr: range * lambdaRange: range * rpr: range + +let tryFindLambda (cursor: pos) (tree: ParsedInput) = + (cursor, tree) + ||> ParsedInput.tryPick (fun path node -> + match node with + | SyntaxNode.SynExpr(SynExpr.Lambda(parsedData = Some([ SynPat.Named(ident = SynIdent(patIdent, _)) ], bodyExpr)) as synExpr) -> + match bodyExpr with + // x.Foo + | LongIdentExprIdentifier identifierIdent + // x.Foo().Bar + | SynExpr.DotGet( + expr = SynExpr.App(ExprAtomicFlag.Atomic, false, LongIdentExprIdentifier identifierIdent, SynExpr.Paren _, _)) + // x.Foo() + | SynExpr.App(ExprAtomicFlag.Atomic, + false, + LongIdentExprIdentifier identifierIdent, + (SynExpr.Const(constant = SynConst.Unit) | SynExpr.Paren _), + _) -> + if identifierIdent.idText <> patIdent.idText then + None + else + let mLambda = + mkRange synExpr.Range.FileName synExpr.Range.Start identifierIdent.idRange.End + + match List.tryHead path with + | Some(SyntaxNode.SynExpr(SynExpr.Paren(leftParenRange = lpr; rightParenRange = Some rpr))) -> + Some(ReplaceInfo.ParenLambda(lpr, mLambda, rpr)) + | _ -> Some(ReplaceInfo.RawLambda mLambda) + | _ -> None + | _ -> None) + +let title = "Replace lambda with _." + +let languageFeature = lazy LanguageFeatureShim("AccessorFunctionShorthand") + +let fix (getLanguageVersion: GetLanguageVersion) (getParseResultsForFile: GetParseResultsForFile) : CodeFix = + fun (codeActionParams: CodeActionParams) -> + asyncResult { + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + let! languageVersion = getLanguageVersion fileName + + if not (languageVersion.SupportsFeature languageFeature.Value) then + return [] + else + + let fcsPos = protocolPosToPos codeActionParams.Range.Start + + let! (parseAndCheckResults: ParseAndCheckResults, _line: string, _sourceText: IFSACSourceText) = + getParseResultsForFile fileName fcsPos + + match tryFindLambda fcsPos parseAndCheckResults.GetParseResults.ParseTree with + | None -> return [] + | Some replaceInfo -> + let edits = + match replaceInfo with + | ReplaceInfo.RawLambda m -> + [| { Range = fcsRangeToLsp m + NewText = "_" } |] + | ReplaceInfo.ParenLambda(lpr, mLambda, rpr) -> + [| { Range = fcsRangeToLsp lpr + NewText = "" } + { Range = fcsRangeToLsp mLambda + NewText = " _" } + { Range = fcsRangeToLsp rpr + NewText = "" } |] + + return + [ { SourceDiagnostic = None + Title = title + File = codeActionParams.TextDocument + Edits = edits + Kind = FixKind.Fix } ] + } diff --git a/src/FsAutoComplete/CodeFixes/ReplaceLambdaWithDotLambda.fsi b/src/FsAutoComplete/CodeFixes/ReplaceLambdaWithDotLambda.fsi new file mode 100644 index 000000000..d04449872 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ReplaceLambdaWithDotLambda.fsi @@ -0,0 +1,6 @@ +module FsAutoComplete.CodeFix.ReplaceLambdaWithDotLambda + +open FsAutoComplete.CodeFix.Types + +val title: string +val fix: getLanguageVersion: GetLanguageVersion -> getParseResultsForFile: GetParseResultsForFile -> CodeFix diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 28ff20d58..2ccff0d76 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -1924,7 +1924,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac RemoveUnnecessaryParentheses.fix forceFindSourceText AddTypeAliasToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile UpdateTypeAbbreviationInSignatureFile.fix tryGetParseAndCheckResultsForFile - AddBindingToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile |]) + AddBindingToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile + ReplaceLambdaWithDotLambda.fix getLanguageVersion tryGetParseAndCheckResultsForFile |]) let forgetDocument (uri: DocumentUri) = async { diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ReplaceLambdaWithDotLambdaTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ReplaceLambdaWithDotLambdaTests.fs new file mode 100644 index 000000000..2af6d1611 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ReplaceLambdaWithDotLambdaTests.fs @@ -0,0 +1,45 @@ +module private FsAutoComplete.Tests.CodeFixTests.ReplaceLambdaWithDotLambdaTests + +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix + +let tests state = + serverTestList (nameof ReplaceLambdaWithDotLambda) state defaultConfigDto None (fun server -> + [ let selectCodeFix = CodeFix.withTitle ReplaceLambdaWithDotLambda.title + + testCaseAsync "Simple property" + <| CodeFix.check + server + "let x = \"\" |> fun y -> $0y.Length" + Diagnostics.acceptAll + selectCodeFix + "let x = \"\" |> _.Length" + + testCaseAsync "Property of application" + <| CodeFix.check + server + "let a5 : {| Foo : int -> {| X : string |} |} -> string = fun x -> x.$0Foo(5).X" + Diagnostics.acceptAll + selectCodeFix + "let a5 : {| Foo : int -> {| X : string |} |} -> string = _.Foo(5).X" + + testCaseAsync "Application" + <| CodeFix.check + server + "let a6 = [1] |> List.map(fun x -> x$0.ToString())" + Diagnostics.acceptAll + selectCodeFix + "let a6 = [1] |> List.map _.ToString()" + + testCaseAsync "fun x -> x.ToString()" + <| CodeFix.check + server + "let a6 = fun$0 x -> x.ToString()" + Diagnostics.acceptAll + selectCodeFix + "let a6 = _.ToString()" + + ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 88ee76a6a..04760bc77 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -3435,4 +3435,5 @@ let tests textFactory state = removeUnnecessaryParenthesesTests state AddTypeAliasToSignatureFileTests.tests state UpdateTypeAbbreviationInSignatureFileTests.tests state - AddBindingToSignatureFileTests.tests state ] + AddBindingToSignatureFileTests.tests state + ReplaceLambdaWithDotLambdaTests.tests state ]