diff --git a/src/FsAutoComplete.Core/ParseAndCheckResults.fs b/src/FsAutoComplete.Core/ParseAndCheckResults.fs index 9c0e68bca..69bd6da20 100644 --- a/src/FsAutoComplete.Core/ParseAndCheckResults.fs +++ b/src/FsAutoComplete.Core/ParseAndCheckResults.fs @@ -74,7 +74,7 @@ type ParseAndCheckResults | None -> return Error "load directive not recognized" } - member __.TryFindIdentifierDeclaration (pos: Position) (lineStr: LineStr) = + member x.TryFindIdentifierDeclaration (pos: Position) (lineStr: LineStr) = match Lexer.findLongIdents (pos.Column, lineStr) with | None -> async.Return(ResultOrString.Error "Could not find ident at this location") | Some (col, identIsland) -> @@ -151,6 +151,15 @@ type ParseAndCheckResults return ResultOrString.Error(sprintf "Could not find declaration. %s" elaboration) | FindDeclResult.DeclFound range when range.FileName.EndsWith(Range.rangeStartup.FileName) -> return ResultOrString.Error "Could not find declaration" + | FindDeclResult.DeclFound range when range.FileName = UMX.untag x.FileName -> + // decl in same file + // necessary to get decl in untitled file (-> `File.Exists range.FileName` is false) + logger.info ( + Log.setMessage "Got a declresult of {range} in same file" + >> Log.addContextDestructured "range" range + ) + + return Ok(FindDeclarationResult.Range range) | FindDeclResult.DeclFound range when System.IO.File.Exists range.FileName -> let rangeStr = range.ToString() diff --git a/src/FsAutoComplete/CodeFixes.fs b/src/FsAutoComplete/CodeFixes.fs index 1634f815b..3a331cab3 100644 --- a/src/FsAutoComplete/CodeFixes.fs +++ b/src/FsAutoComplete/CodeFixes.fs @@ -73,6 +73,78 @@ module Types = Command = None Data = None } +module SourceText = + let inline private assertLineIndex lineIndex (sourceText: ISourceText) = + assert(0 <= lineIndex && lineIndex < sourceText.GetLineCount()) + /// Note: this fails when `sourceText` is empty string (`""`) + /// -> No lines + /// Use `WithEmptyHandling.isFirstLine` to handle empty string + let isFirstLine lineIndex (sourceText: ISourceText) = + assertLineIndex lineIndex sourceText + lineIndex = 0 + /// Note: this fails when `sourceText` is empty string (`""`) + /// -> No lines + /// Use `WithEmptyHandling.isLastLine` to handle empty string + let isLastLine lineIndex (sourceText: ISourceText) = + assertLineIndex lineIndex sourceText + lineIndex = sourceText.GetLineCount() - 1 + + /// SourceText treats empty string as no source: + /// ```fsharp + /// let text = SourceText.ofString "" + /// assert(text.ToString() = "") + /// assert(text.GetLastCharacterPosition() = (0, 0)) // Note: first line is `1` + /// assert(text.GetLineCount() = 0) // Note: `(SourceText.ofString "\n").GetLineCount()` is `2` + /// assert(text.GetLineString 0 ) // System.IndexOutOfRangeException: Index was outside the bounds of the array. + /// ``` + /// -> Functions in here treat empty string as empty single line + /// + /// Note: There's always at least empty single line + /// -> source MUST at least be empty (cannot not exist) + module WithEmptyHandling = + let getLineCount (sourceText: ISourceText) = + match sourceText.GetLineCount () with + | 0 -> 1 + | c -> c + // or + // max 1 (sourceText.GetLineCount()) + + let inline private assertLineIndex lineIndex sourceText = + assert(0 <= lineIndex && lineIndex < getLineCount sourceText) + + let getLineString lineIndex (sourceText: ISourceText) = + assertLineIndex lineIndex sourceText + if lineIndex = 0 && sourceText.GetLineCount() = 0 then + "" + else + sourceText.GetLineString lineIndex + + let isFirstLine lineIndex (sourceText: ISourceText) = + assertLineIndex lineIndex sourceText + // No need to check for inside `getLineCount`: there's always at least one line (might be empty) + lineIndex = 0 + + let isLastLine lineIndex (sourceText: ISourceText) = + assertLineIndex lineIndex sourceText + lineIndex = (getLineCount sourceText) - 1 + + /// Returns position after last character in specified line. + /// Same as line length. + /// + /// Example: + /// ```fsharp + /// let text = SourceText.ofString "let a = 2\nlet foo = 42\na + foo\n" + /// + /// assert(afterLastCharacterPosition 0 text = 9) + /// assert(afterLastCharacterPosition 1 text = 12) + /// assert(afterLastCharacterPosition 2 text = 7) + /// assert(afterLastCharacterPosition 2 text = 0) + /// ``` + let afterLastCharacterPosition lineIndex (sourceText: ISourceText) = + assertLineIndex lineIndex sourceText + let line = sourceText |> getLineString lineIndex + line.Length + /// helpers for iterating along text lines module Navigation = @@ -144,6 +216,51 @@ module Navigation = let walkForwardUntilCondition lines pos condition = walkForwardUntilConditionWithTerminal lines pos condition (fun _ -> false) + /// Tries to detect the last cursor position in line before `currentLine` (0-based). + /// + /// Returns `None` iff there's no prev line -> `currentLine` is first line + let tryEndOfPrevLine (lines: ISourceText) currentLine = + if SourceText.WithEmptyHandling.isFirstLine currentLine lines then + None + else + let prevLine = currentLine - 1 + { Line = prevLine; Character = lines |> SourceText.WithEmptyHandling.afterLastCharacterPosition prevLine } + |> Some + /// Tries to detect the first cursor position in line after `currentLine` (0-based). + /// + /// Returns `None` iff there's no next line -> `currentLine` is last line + let tryStartOfNextLine (lines: ISourceText) currentLine = + if SourceText.WithEmptyHandling.isLastLine currentLine lines then + None + else + let nextLine = currentLine + 1 + { Line = nextLine; Character = 0 } + |> Some + + /// Gets the range to delete the complete line `lineIndex` (0-based). + /// Deleting the line includes a linebreak if possible + /// -> range starts either at end of previous line (-> includes leading linebreak) + /// or start of next line (-> includes trailing linebreak) + /// + /// Special case: there's just one line + /// -> delete text of (single) line + let rangeToDeleteFullLine lineIndex (lines: ISourceText) = + match tryEndOfPrevLine lines lineIndex with + | Some start -> + // delete leading linebreak + { Start = start; End = { Line = lineIndex; Character = lines |> SourceText.WithEmptyHandling.afterLastCharacterPosition lineIndex } } + | None -> + match tryStartOfNextLine lines lineIndex with + | Some fin -> + // delete trailing linebreak + { Start = { Line = lineIndex; Character = 0 }; End = fin } + | None -> + // single line + // -> just delete all text in line + { Start = { Line = lineIndex; Character = 0 }; End = { Line = lineIndex; Character = lines |> SourceText.WithEmptyHandling.afterLastCharacterPosition lineIndex } } + + + module Run = open Types diff --git a/src/FsAutoComplete/CodeFixes/AddMissingEqualsToTypeDefinition.fs b/src/FsAutoComplete/CodeFixes/AddMissingEqualsToTypeDefinition.fs new file mode 100644 index 000000000..a501908e0 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AddMissingEqualsToTypeDefinition.fs @@ -0,0 +1,36 @@ +module FsAutoComplete.CodeFix.AddMissingEqualsToTypeDefinition + +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix.Navigation +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers + +let title = "Add missing '=' to type definition" +/// a codefix that adds in missing '=' characters in type declarations +let fix (getFileLines: GetFileLines) = + Run.ifDiagnosticByCode + (Set.ofList [ "3360" ]) + (fun diagnostic codeActionParams -> + asyncResult { + let fileName = + codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + + let! lines = getFileLines fileName + let! walkPos = dec lines diagnostic.Range.Start |> Result.ofOption (fun _ -> "No walk pos") + match walkBackUntilCondition lines walkPos (System.Char.IsWhiteSpace >> not) with + | Some firstNonWhitespaceChar -> + let! insertPos = inc lines firstNonWhitespaceChar |> Result.ofOption (fun _ -> "No insert pos") + + return + [ { SourceDiagnostic = Some diagnostic + Title = title + File = codeActionParams.TextDocument + Edits = + [| { Range = { Start = insertPos; End = insertPos } + NewText = "= " } |] + Kind = FixKind.Fix } ] + | None -> return [] + } + ) diff --git a/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs b/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs index 7f93d7cf5..393e66007 100644 --- a/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs +++ b/src/FsAutoComplete/CodeFixes/AddMissingRecKeyword.fs @@ -8,6 +8,7 @@ open FsAutoComplete open FsAutoComplete.LspHelpers open FSharp.UMX +let title symbolName = $"Make '{symbolName}' recursive" /// a codefix that adds the 'rec' modifier to a binding in a mutually-recursive loop let fix (getFileLines: GetFileLines) (getLineText: GetLineText): CodeFix = Run.ifDiagnosticByCode @@ -60,10 +61,10 @@ let fix (getFileLines: GetFileLines) (getLineText: GetLineText): CodeFix = let protocolRange = fcsRangeToLsp (FSharp.Compiler.Text.Range.mkRange (UMX.untag fileName) fcsStartPos fcsEndPos) - let symbolName = getLineText lines protocolRange + let! symbolName = getLineText lines protocolRange return - [ { Title = $"Make '{symbolName}' recursive" + [ { Title = title symbolName File = codeActionParams.TextDocument SourceDiagnostic = Some diagnostic Edits = diff --git a/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs b/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs new file mode 100644 index 000000000..454a1e593 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/AddNewKeywordToDisposableConstructorInvocation.fs @@ -0,0 +1,23 @@ +module FsAutoComplete.CodeFix.AddNewKeywordToDisposableConstructorInvocation + +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers + +let title = "Add 'new'" +/// a codefix that suggests using the 'new' keyword on IDisposables +let fix (getRangeText: GetRangeText) = + Run.ifDiagnosticByCode + (Set.ofList [ "760" ]) + (fun diagnostic codeActionParams -> + AsyncResult.retn [ { SourceDiagnostic = Some diagnostic + File = codeActionParams.TextDocument + Title = title + Edits = + [| { Range = { Start = diagnostic.Range.Start; End = diagnostic.Range.Start } + NewText = $"new " } |] + Kind = FixKind.Refactor } ] + ) diff --git a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs index 1c300da35..a232e09b8 100644 --- a/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs +++ b/src/FsAutoComplete/CodeFixes/AddTypeToIndeterminateValue.fs @@ -7,7 +7,9 @@ open FsAutoComplete open FsAutoComplete.LspHelpers open FSharp.Compiler.EditorServices open FSharp.Compiler.Symbols +open FSharp.UMX +let title = "Add explicit type annotation" /// fix inderminate type errors by adding an explicit type to a value let fix (getParseResultsForFile: GetParseResultsForFile) @@ -17,14 +19,13 @@ let fix (Set.ofList ["72"; "3245"]) (fun diagnostic codeActionParams -> asyncResult { - let filename = codeActionParams.TextDocument.GetFilePath () - let typedFileName = filename |> Utils.normalizePath + let fileName = codeActionParams.TextDocument.GetFilePath () |> Utils.normalizePath let fcsRange = protocolRangeToRange (codeActionParams.TextDocument.GetFilePath()) diagnostic.Range - let! (tyRes, line, lines) = getParseResultsForFile typedFileName fcsRange.Start + let! (tyRes, line, lines) = getParseResultsForFile fileName fcsRange.Start let! (endColumn, identIslands) = Lexer.findLongIdents(fcsRange.Start.Column, line) |> Result.ofOption (fun _ -> "No long ident at position") match tyRes.GetCheckResults.GetDeclarationLocation(fcsRange.Start.Line, endColumn, line, List.ofArray identIslands) with - | FindDeclResult.DeclFound declRange when declRange.FileName = filename -> - let! projectOptions = getProjectOptionsForFile typedFileName + | FindDeclResult.DeclFound declRange when declRange.FileName = UMX.untag fileName -> + let! projectOptions = getProjectOptionsForFile fileName let protocolDeclRange = fcsRangeToLsp declRange let! declText = lines.GetText declRange let! declTextLine = lines.GetLine declRange.Start |> Result.ofOption (fun _ -> "No line found at pos") @@ -48,7 +49,7 @@ let fix else "(" + declText + ": " + typeString + ")", protocolDeclRange return [{ - Title = "Add explicit type annotation" + Title = title File = codeActionParams.TextDocument SourceDiagnostic = Some diagnostic Kind = FixKind.Fix diff --git a/src/FsAutoComplete/CodeFixes/ChangeCSharpLambdaToFSharp.fs b/src/FsAutoComplete/CodeFixes/ChangeCSharpLambdaToFSharp.fs deleted file mode 100644 index a0b6b64e2..000000000 --- a/src/FsAutoComplete/CodeFixes/ChangeCSharpLambdaToFSharp.fs +++ /dev/null @@ -1,44 +0,0 @@ -/// a codefix that rewrites C#-style '=>' lambdas to F#-style 'fun _ -> _' lambdas -module FsAutoComplete.CodeFix.ChangeCSharpLambdaToFSharp - -open FsToolkit.ErrorHandling -open FsAutoComplete.CodeFix.Types -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers - -let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineText) - : CodeFix = - Run.ifDiagnosticByCode - (Set.ofList [ "39" // undefined value - "43" ]) // operator not defined - (fun diagnostic codeActionParams -> - asyncResult { - let fileName = - codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - - let fcsPos = protocolPosToPos diagnostic.Range.Start - let! (tyRes, _, lines) = getParseResultsForFile fileName fcsPos - - match tyRes.GetParseResults.TryRangeOfParenEnclosingOpEqualsGreaterUsage fcsPos with - | Some (fullParenRange, lambdaArgRange, lambdaBodyRange) -> - let! argExprText = - getLineText lines (fcsRangeToLsp lambdaArgRange) - - let! bodyExprText = - getLineText lines (fcsRangeToLsp lambdaBodyRange) - - let replacementText = $"fun {argExprText} -> {bodyExprText}" - let replacementRange = fcsRangeToLsp fullParenRange - - return - [ { Title = "Replace C#-style lambda with F# lambda" - File = codeActionParams.TextDocument - SourceDiagnostic = Some diagnostic - Edits = - [| { Range = replacementRange - NewText = replacementText } |] - Kind = FixKind.Refactor } ] - | None -> return [] - } - ) diff --git a/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fs b/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fs new file mode 100644 index 000000000..1e5100401 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ChangeDerefBangToValue.fs @@ -0,0 +1,60 @@ +/// replace use of ! operator on ref cells with calls to .Value +module FsAutoComplete.CodeFix.ChangeDerefBangToValue + +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text.Range +open FSharp.UMX + +/// adopted from `dotnet/fsharp` -> `FSharp.Compiler.CodeAnalysis.FSharpParseFileResults.TryRangeOfExpressionBeingDereferencedContainingPos` +let private tryGetRangeOfDeref input derefPos = + SyntaxTraversal.Traverse(derefPos, input, { new SyntaxVisitorBase<_>() with + member _.VisitExpr(_, _, defaultTraverse, expr) = + match expr with + | SynExpr.App(_, false, SynExpr.Ident funcIdent, expr, _) -> + if funcIdent.idText = "op_Dereference" && rangeContainsPos funcIdent.idRange derefPos then + Some (funcIdent.idRange, expr.Range) + else + None + | _ -> defaultTraverse expr }) + +let title = "Use `.Value` instead of dereference operator" +let fix + (getParseResultsForFile: GetParseResultsForFile) + (getLineText: GetLineText) + : CodeFix = + Run.ifDiagnosticByCode + (Set.ofList ["3370"]) + (fun diagnostic codeActionParams -> asyncResult { + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + + let derefOpRange = protocolRangeToRange (UMX.untag fileName) diagnostic.Range + let! parseResults, _, _ = getParseResultsForFile fileName derefOpRange.Start + + let! (derefOpRange', exprRange) = + tryGetRangeOfDeref parseResults.GetParseResults.ParseTree derefOpRange.End + |> Result.ofOption (fun _ -> "No deref found at that pos") + assert(derefOpRange = derefOpRange') + + return [ + { + Title = title + File = codeActionParams.TextDocument + SourceDiagnostic = None + Kind = FixKind.Refactor + Edits = [| + // remove leading `!` (and whitespaces after `!`) + { Range = { Start = fcsPosToLsp derefOpRange'.Start; End = fcsPosToLsp exprRange.Start } + NewText = "" } + // Append trailing `.Value` + { Range = + let lspPos = fcsPosToLsp exprRange.End + { Start = lspPos; End = lspPos } + NewText = ".Value" } + |] + } + ] + }) diff --git a/src/FsAutoComplete/CodeFixes/UseSafeCastInsteadOfUnsafe.fs b/src/FsAutoComplete/CodeFixes/ChangeDowncastToUpcast.fs similarity index 86% rename from src/FsAutoComplete/CodeFixes/UseSafeCastInsteadOfUnsafe.fs rename to src/FsAutoComplete/CodeFixes/ChangeDowncastToUpcast.fs index db61a88ee..e7718d3fa 100644 --- a/src/FsAutoComplete/CodeFixes/UseSafeCastInsteadOfUnsafe.fs +++ b/src/FsAutoComplete/CodeFixes/ChangeDowncastToUpcast.fs @@ -1,4 +1,4 @@ -module FsAutoComplete.CodeFix.UseSafeCastInsteadOfUnsafe +module FsAutoComplete.CodeFix.ChangeDowncastToUpcast open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types @@ -6,6 +6,8 @@ open Ionide.LanguageServerProtocol.Types open FsAutoComplete open FsAutoComplete.LspHelpers +let titleUpcastOperator = "Use ':>' operator" +let titleUpcastFunction = "Use 'upcast' function" /// a codefix that replaces unsafe casts with safe casts let fix (getRangeText: GetRangeText): CodeFix = Run.ifDiagnosticByCode @@ -23,7 +25,7 @@ let fix (getRangeText: GetRangeText): CodeFix = | true, false -> AsyncResult.retn [ { File = codeActionParams.TextDocument SourceDiagnostic = Some diagnostic - Title = "Use ':>' operator" + Title = titleUpcastOperator Edits = [| { Range = diagnostic.Range NewText = expressionText.Replace(":?>", ":>") } |] @@ -31,7 +33,7 @@ let fix (getRangeText: GetRangeText): CodeFix = | false, true -> AsyncResult.retn [ { File = codeActionParams.TextDocument SourceDiagnostic = Some diagnostic - Title = "Use 'upcast' function" + Title = titleUpcastFunction Edits = [| { Range = diagnostic.Range NewText = expressionText.Replace("downcast", "upcast") } |] diff --git a/src/FsAutoComplete/CodeFixes/ColonInFieldType.fs b/src/FsAutoComplete/CodeFixes/ChangeEqualsInFieldTypeToColon.fs similarity index 83% rename from src/FsAutoComplete/CodeFixes/ColonInFieldType.fs rename to src/FsAutoComplete/CodeFixes/ChangeEqualsInFieldTypeToColon.fs index ab051801e..21b36ec3b 100644 --- a/src/FsAutoComplete/CodeFixes/ColonInFieldType.fs +++ b/src/FsAutoComplete/CodeFixes/ChangeEqualsInFieldTypeToColon.fs @@ -1,9 +1,10 @@ -module FsAutoComplete.CodeFix.ColonInFieldType +module FsAutoComplete.CodeFix.ChangeEqualsInFieldTypeToColon open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types open FsAutoComplete +let title = "Use ':' for type in field declaration" /// a codefix that fixes a malformed record type annotation to use colon instead of equals let fix: CodeFix = Run.ifDiagnosticByCode @@ -11,7 +12,7 @@ let fix: CodeFix = (fun diagnostic codeActionParams -> if diagnostic.Message = "Unexpected symbol '=' in field declaration. Expected ':' or other token." then AsyncResult.retn [ { File = codeActionParams.TextDocument - Title = "Use ':' for type in field declaration" + Title = title SourceDiagnostic = Some diagnostic Edits = [| { Range = diagnostic.Range diff --git a/src/FsAutoComplete/CodeFixes/NegationToSubtraction.fs b/src/FsAutoComplete/CodeFixes/ChangePrefixNegationToInfixSubtraction.fs similarity index 94% rename from src/FsAutoComplete/CodeFixes/NegationToSubtraction.fs rename to src/FsAutoComplete/CodeFixes/ChangePrefixNegationToInfixSubtraction.fs index b3dea2537..0716a5242 100644 --- a/src/FsAutoComplete/CodeFixes/NegationToSubtraction.fs +++ b/src/FsAutoComplete/CodeFixes/ChangePrefixNegationToInfixSubtraction.fs @@ -1,4 +1,4 @@ -module FsAutoComplete.CodeFix.NegationToSubtraction +module FsAutoComplete.CodeFix.ChangePrefixNegationToInfixSubtraction open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Navigation diff --git a/src/FsAutoComplete/CodeFixes/RefCellAccessToNot.fs b/src/FsAutoComplete/CodeFixes/ChangeRefCellDerefToNot.fs similarity index 89% rename from src/FsAutoComplete/CodeFixes/RefCellAccessToNot.fs rename to src/FsAutoComplete/CodeFixes/ChangeRefCellDerefToNot.fs index 5fb38bb30..017955784 100644 --- a/src/FsAutoComplete/CodeFixes/RefCellAccessToNot.fs +++ b/src/FsAutoComplete/CodeFixes/ChangeRefCellDerefToNot.fs @@ -1,4 +1,4 @@ -module FsAutoComplete.CodeFix.RefCellAccesToNot +module FsAutoComplete.CodeFix.ChangeRefCellDerefToNot open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types @@ -6,6 +6,7 @@ open Ionide.LanguageServerProtocol.Types open FsAutoComplete open FsAutoComplete.LspHelpers +let title = "Use 'not' to negate expression" /// a codefix that changes a ref cell deref (!) to a call to 'not' let fix (getParseResultsForFile: GetParseResultsForFile): CodeFix = Run.ifDiagnosticByCode @@ -22,7 +23,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile): CodeFix = | Some derefRange -> return [ { SourceDiagnostic = Some diagnostic - Title = "Use 'not' to negate expression" + Title = title File = codeActionParams.TextDocument Edits = [| { Range = fcsRangeToLsp derefRange diff --git a/src/FsAutoComplete/CodeFixes/ConvertBangEqualsToInequality.fs b/src/FsAutoComplete/CodeFixes/ConvertBangEqualsToInequality.fs index 957591a56..9d2c8b640 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertBangEqualsToInequality.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertBangEqualsToInequality.fs @@ -6,6 +6,7 @@ open FsAutoComplete.CodeFix.Types open FsAutoComplete open FsAutoComplete.LspHelpers +let title = "Use <> for inequality check" let fix (getRangeText: GetRangeText): CodeFix = Run.ifDiagnosticByCode (Set.ofList ["43"]) @@ -14,7 +15,7 @@ let fix (getRangeText: GetRangeText): CodeFix = let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath let! errorText = getRangeText fileName diag.Range do! Result.guard (fun () -> errorText = "!=") "Not an != equality usage" - return [{ Title = "Use <> for inequality check" + return [{ Title = title File = codeActionParams.TextDocument SourceDiagnostic = Some diag Kind = FixKind.Fix diff --git a/src/FsAutoComplete/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs b/src/FsAutoComplete/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs new file mode 100644 index 000000000..f014e6e73 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ConvertCSharpLambdaToFSharpLambda.fs @@ -0,0 +1,76 @@ +/// a codefix that rewrites C#-style '=>' lambdas to F#-style 'fun _ -> _' lambdas +module FsAutoComplete.CodeFix.ConvertCSharpLambdaToFSharpLambda + +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text + +let title = "Replace C#-style lambda with F# lambda" +// adopted from `FSharp.Compiler.CodeAnalysis.FSharpParseFileResults.TryRangeOfParenEnclosingOpEqualsGreaterUsage` +let private tryRangeOfParenEnclosingOpEqualsGreaterUsage input pos = + let (|Ident|_|) ofName = + function | SynExpr.Ident ident when ident.idText = ofName -> Some () + | _ -> None + let (|InfixAppOfOpEqualsGreater|_|) = + function | SynExpr.App(ExprAtomicFlag.NonAtomic, false, SynExpr.App(ExprAtomicFlag.NonAtomic, true, Ident "op_EqualsGreater", actualParamListExpr, range), actualLambdaBodyExpr, _) -> + let opEnd = range.End + let opStart = Position.mkPos (range.End.Line) (range.End.Column - 2) + let opRange = Range.mkRange range.FileName opStart opEnd + + let argsRange = actualParamListExpr.Range + + Some (argsRange, opRange) + | _ -> None + + SyntaxTraversal.Traverse(pos, input, { new SyntaxVisitorBase<_>() with + member _.VisitExpr(_, _, defaultTraverse, expr) = + match expr with + | SynExpr.Paren(InfixAppOfOpEqualsGreater(argsRange, opRange), _, _, _) -> + Some (argsRange, opRange) + | _ -> defaultTraverse expr + + member _.VisitBinding(_path, defaultTraverse, binding) = + match binding with + | SynBinding(kind=SynBindingKind.Normal; expr=InfixAppOfOpEqualsGreater(argsRange, opRange)) -> + Some (argsRange, opRange) + | _ -> defaultTraverse binding }) +let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineText) + : CodeFix = + Run.ifDiagnosticByCode + (Set.ofList [ "39" // undefined value + "43" ]) // operator not defined + (fun diagnostic codeActionParams -> + asyncResult { + let fileName = + codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + + let fcsPos = protocolPosToPos diagnostic.Range.Start + let! (tyRes, _, lines) = getParseResultsForFile fileName fcsPos + + match tryRangeOfParenEnclosingOpEqualsGreaterUsage tyRes.GetAST fcsPos with + | Some (argsRange, opRange) -> + return + [ { Title = title + File = codeActionParams.TextDocument + SourceDiagnostic = Some diagnostic + Edits = + [| + // add `fun ` in front of args + { + Range = { Start = fcsPosToLsp argsRange.Start; End = fcsPosToLsp argsRange.Start } + NewText = "fun " + } + // replace `=>` with `->` + { + Range = fcsRangeToLsp opRange + NewText = "->" + } + |] + Kind = FixKind.Refactor } ] + | None -> return [] + } + ) diff --git a/src/FsAutoComplete/CodeFixes/DoubleEqualsToSingleEquals.fs b/src/FsAutoComplete/CodeFixes/ConvertDoubleEqualsToSingleEquals.fs similarity index 86% rename from src/FsAutoComplete/CodeFixes/DoubleEqualsToSingleEquals.fs rename to src/FsAutoComplete/CodeFixes/ConvertDoubleEqualsToSingleEquals.fs index 82d3fcd2a..654a0ab51 100644 --- a/src/FsAutoComplete/CodeFixes/DoubleEqualsToSingleEquals.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertDoubleEqualsToSingleEquals.fs @@ -1,10 +1,11 @@ -module FsAutoComplete.CodeFix.DoubleEqualsToSingleEquals +module FsAutoComplete.CodeFix.ConvertDoubleEqualsToSingleEquals open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types open FsAutoComplete open FsAutoComplete.LspHelpers +let title = "Use '=' for equality check" /// a codefix that corrects == equality to = equality let fix (getRangeText: GetRangeText) : CodeFix = Run.ifDiagnosticByCode @@ -16,7 +17,7 @@ let fix (getRangeText: GetRangeText) : CodeFix = match errorText with | "==" -> return - [ { Title = "Use '=' for equality check" + [ { Title = title File = codeActionParams.TextDocument SourceDiagnostic = Some diagnostic Edits = diff --git a/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs b/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs index a79e4c27d..5cdac49f8 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertInvalidRecordToAnonRecord.fs @@ -7,6 +7,7 @@ open FsAutoComplete open FsAutoComplete.CodeFix.Navigation open FsAutoComplete.LspHelpers +let title = "Convert to anonymous record" /// a codefix that converts unknown/partial record expressions to anonymous records let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = Run.ifDiagnosticByCode @@ -34,7 +35,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = |> Result.ofOption (fun _ -> "No end insert range") return - [ { Title = "Convert to anonymous record" + [ { Title = title File = codeActionParams.TextDocument SourceDiagnostic = Some diagnostic Edits = diff --git a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs index a7111aac3..6b425be6a 100644 --- a/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs +++ b/src/FsAutoComplete/CodeFixes/ConvertPositionalDUToNamed.fs @@ -98,7 +98,7 @@ let private createEdit (astField: SynPat, duField: string) : TextEdit list = { NewText = suffix; Range = endRange } ] let private createWildCard endRange (duField: string) : TextEdit = - let wildcard = $"{duField} = _;" + let wildcard = $" {duField} = _;" let range = endRange { NewText = wildcard; Range = range } diff --git a/src/FsAutoComplete/CodeFixes/GenerateAbstractClassStub.fs b/src/FsAutoComplete/CodeFixes/GenerateAbstractClassStub.fs index f3ff5d07f..6f9873b8c 100644 --- a/src/FsAutoComplete/CodeFixes/GenerateAbstractClassStub.fs +++ b/src/FsAutoComplete/CodeFixes/GenerateAbstractClassStub.fs @@ -15,23 +15,14 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getTextReplacements: unit -> Map) : CodeFix = Run.ifDiagnosticByCode - (Set.ofList [ "365"; "54" ]) + (Set.ofList [ "365" ]) (fun diagnostic codeActionParams -> asyncResult { let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - let interestingRange = - (match diagnostic.Code with - | Some "365" -> - // the object expression diagnostic covers the entire interesting range - diagnostic.Range - | Some "54" -> - // the full-class range is on the typename, which should be enough to enable traversal - diagnostic.Range - | _ -> - // everything else is a best guess - codeActionParams.Range) + // the object expression diagnostic covers the entire interesting range + let interestingRange = diagnostic.Range let fcsRange = interestingRange |> protocolRangeToRange (UMX.untag fileName) diff --git a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs index 3c1b5d482..0a698906d 100644 --- a/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs +++ b/src/FsAutoComplete/CodeFixes/MakeDeclarationMutable.fs @@ -7,6 +7,7 @@ open FsAutoComplete open FsAutoComplete.LspHelpers open FSharp.UMX +let title = "Make declaration 'mutable'" /// a codefix that makes a binding mutable when a user attempts to mutably set it let fix (getParseResultsForFile: GetParseResultsForFile) (getProjectOptionsForFile: GetProjectOptionsForFile) @@ -34,7 +35,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) return [ { File = codeActionParams.TextDocument SourceDiagnostic = Some diagnostic - Title = "Make declaration 'mutable'" + Title = title Edits = [| { Range = { Start = lspRange.Start diff --git a/src/FsAutoComplete/CodeFixes/MissingEquals.fs b/src/FsAutoComplete/CodeFixes/MissingEquals.fs deleted file mode 100644 index 9bbed1e91..000000000 --- a/src/FsAutoComplete/CodeFixes/MissingEquals.fs +++ /dev/null @@ -1,39 +0,0 @@ -module FsAutoComplete.CodeFix.MissingEquals - -open FsToolkit.ErrorHandling -open FsAutoComplete.CodeFix.Navigation -open FsAutoComplete.CodeFix.Types -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers - -/// a codefix that adds in missing '=' characters in type declarations -let fix (getFileLines: GetFileLines) = - Run.ifDiagnosticByCode - (Set.ofList [ "10"; "3360" ]) - (fun diagnostic codeActionParams -> - asyncResult { - if diagnostic.Message.Contains "Unexpected symbol '{' in type definition" - || diagnostic.Message.Contains "Unexpected keyword 'member' in type definition" then - let fileName = - codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - - let! lines = getFileLines fileName - let! walkPos = dec lines diagnostic.Range.Start |> Result.ofOption (fun _ -> "No walk pos") - match walkBackUntilCondition lines walkPos (System.Char.IsWhiteSpace >> not) with - | Some firstNonWhitespaceChar -> - let! insertPos = inc lines firstNonWhitespaceChar |> Result.ofOption (fun _ -> "No insert pos") - - return - [ { SourceDiagnostic = Some diagnostic - Title = "Add missing '=' to type definition" - File = codeActionParams.TextDocument - Edits = - [| { Range = { Start = insertPos; End = insertPos } - NewText = " =" } |] - Kind = FixKind.Fix } ] - | None -> return [] - else - return [] - } - ) diff --git a/src/FsAutoComplete/CodeFixes/NewWithDisposables.fs b/src/FsAutoComplete/CodeFixes/NewWithDisposables.fs deleted file mode 100644 index e9016a21c..000000000 --- a/src/FsAutoComplete/CodeFixes/NewWithDisposables.fs +++ /dev/null @@ -1,24 +0,0 @@ -module FsAutoComplete.CodeFix.NewWithDisposables - -open FsToolkit.ErrorHandling -open FsAutoComplete.CodeFix -open FsAutoComplete.CodeFix.Types -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers - -/// a codefix that suggests using the 'new' keyword on IDisposables -let fix (getRangeText: GetRangeText) = - Run.ifDiagnosticByMessage - "It is recommended that objects supporting the IDisposable interface are created using the syntax" - (fun diagnostic codeActionParams -> - match getRangeText (codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath) diagnostic.Range with - | Ok errorText -> - AsyncResult.retn [ { SourceDiagnostic = Some diagnostic - File = codeActionParams.TextDocument - Title = "Add new" - Edits = - [| { Range = diagnostic.Range - NewText = $"new %s{errorText}" } |] - Kind = FixKind.Refactor } ] - | Error _ -> AsyncResult.retn []) diff --git a/src/FsAutoComplete/CodeFixes/ParenthesizeExpression.fs b/src/FsAutoComplete/CodeFixes/ParenthesizeExpression.fs deleted file mode 100644 index ecc095a04..000000000 --- a/src/FsAutoComplete/CodeFixes/ParenthesizeExpression.fs +++ /dev/null @@ -1,23 +0,0 @@ -module FsAutoComplete.CodeFix.ParenthesizeExpression - -open FsToolkit.ErrorHandling -open FsAutoComplete.CodeFix.Types -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers - -/// a codefix that parenthesizes a member expression that needs it -let fix (getRangeText: GetRangeText): CodeFix = - Run.ifDiagnosticByCode - (Set.ofList [ "597" ]) - (fun diagnostic codeActionParams -> - match getRangeText (codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath) diagnostic.Range with - | Ok erroringExpression -> - AsyncResult.retn [ { Title = "Wrap expression in parentheses" - File = codeActionParams.TextDocument - SourceDiagnostic = Some diagnostic - Edits = - [| { Range = diagnostic.Range - NewText = $"(%s{erroringExpression})" } |] - Kind = FixKind.Fix } ] - | Error _ -> AsyncResult.retn []) diff --git a/src/FsAutoComplete/CodeFixes/RedundantQualifier.fs b/src/FsAutoComplete/CodeFixes/RemoveRedundantQualifier.fs similarity index 82% rename from src/FsAutoComplete/CodeFixes/RedundantQualifier.fs rename to src/FsAutoComplete/CodeFixes/RemoveRedundantQualifier.fs index 7d68a84dc..284ff29df 100644 --- a/src/FsAutoComplete/CodeFixes/RedundantQualifier.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveRedundantQualifier.fs @@ -1,10 +1,11 @@ -module FsAutoComplete.CodeFix.RedundantQualifier +module FsAutoComplete.CodeFix.RemoveRedundantQualifier open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix open FsAutoComplete.CodeFix.Types open Ionide.LanguageServerProtocol.Types +let title = "Remove redundant qualifier" /// a codefix that removes unnecessary qualifiers from an identifier let fix = Run.ifDiagnosticByMessage @@ -14,6 +15,6 @@ let fix = [| { Range = diagnostic.Range NewText = "" } |] File = codeActionParams.TextDocument - Title = "Remove redundant qualifier" + Title = title SourceDiagnostic = Some diagnostic Kind = FixKind.Refactor } ]) diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs index 3237cafde..a877e5e14 100644 --- a/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs +++ b/src/FsAutoComplete/CodeFixes/RemoveUnnecessaryReturnOrYield.fs @@ -4,9 +4,9 @@ open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types open Ionide.LanguageServerProtocol.Types open FsAutoComplete -open FsAutoComplete.CodeFix.Navigation open FsAutoComplete.LspHelpers +let title keyword = $"Remove '%s{keyword}'" /// a codefix that removes 'return' or 'yield' (or bang-variants) when the compiler says they're not necessary let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineText): CodeFix = Run.ifDiagnosticByCode @@ -29,13 +29,13 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineTe let! title = if errorText.StartsWith "return!" - then Ok "Remove 'return!'" + then Ok (title "return!") elif errorText.StartsWith "yield!" - then Ok "Remove 'yield!'" + then Ok (title "yield!") elif errorText.StartsWith "return" - then Ok "Remove 'return'" + then Ok (title "return") elif errorText.StartsWith "yield" - then Ok "Remove 'yield'" + then Ok (title "yield") else Error "unknown start token for remove return or yield codefix" return diff --git a/src/FsAutoComplete/CodeFixes/RemoveUnusedOpens.fs b/src/FsAutoComplete/CodeFixes/RemoveUnusedOpens.fs new file mode 100644 index 000000000..b12b72b28 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/RemoveUnusedOpens.fs @@ -0,0 +1,30 @@ +module FsAutoComplete.CodeFix.RemoveUnusedOpens + +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.CodeFix +open FsAutoComplete.CodeFix.Types +open FsToolkit.ErrorHandling +open FsAutoComplete +open FsAutoComplete.LspHelpers +open FsAutoComplete.CodeFix.Navigation + +let title = "Remove unused open" +/// a codefix that removes unused open statements from the source +let fix (getFileLines: GetFileLines) : CodeFix = + Run.ifDiagnosticByMessage + "Unused open statement" + (fun d codeActionParams -> asyncResult { + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + let! lines = getFileLines fileName + + let lineToRemove = d.Range.Start.Line + let range = lines |> rangeToDeleteFullLine lineToRemove + + return [ + { Edits = [| { Range = range; NewText = "" } |] + File = codeActionParams.TextDocument + Title = title + SourceDiagnostic = Some d + Kind = FixKind.Refactor } + ] + }) diff --git a/src/FsAutoComplete/CodeFixes/UnusedValue.fs b/src/FsAutoComplete/CodeFixes/RenameUnusedValue.fs similarity index 97% rename from src/FsAutoComplete/CodeFixes/UnusedValue.fs rename to src/FsAutoComplete/CodeFixes/RenameUnusedValue.fs index a5d93df70..5198b9719 100644 --- a/src/FsAutoComplete/CodeFixes/UnusedValue.fs +++ b/src/FsAutoComplete/CodeFixes/RenameUnusedValue.fs @@ -1,4 +1,4 @@ -module FsAutoComplete.CodeFix.UnusedValue +module FsAutoComplete.CodeFix.RenameUnusedValue open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix diff --git a/src/FsAutoComplete/CodeFixes/ReplaceBangWithValueFunction.fs b/src/FsAutoComplete/CodeFixes/ReplaceBangWithValueFunction.fs deleted file mode 100644 index a77d648e6..000000000 --- a/src/FsAutoComplete/CodeFixes/ReplaceBangWithValueFunction.fs +++ /dev/null @@ -1,29 +0,0 @@ -/// replace use of ! operator on ref cells with calls to .Value -module FsAutoComplete.CodeFix.ReplaceBangWithValueFunction - -open FsToolkit.ErrorHandling -open FsAutoComplete.CodeFix.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers - -let fix (getParseResultsForFile: GetParseResultsForFile) (getLineText: GetLineText): CodeFix = - fun codeActionParams -> - asyncResult { - let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - let selectionRange = protocolRangeToRange (codeActionParams.TextDocument.GetFilePath()) codeActionParams.Range - let! parseResults, line, lines = getParseResultsForFile fileName selectionRange.Start - let! derefRange = parseResults.GetParseResults.TryRangeOfRefCellDereferenceContainingPos selectionRange.Start |> Result.ofOption (fun _ -> "No deref found at that pos") - let! exprRange = parseResults.GetParseResults.TryRangeOfExpressionBeingDereferencedContainingPos selectionRange.Start |> Result.ofOption (fun _ -> "No expr found at that pos") - let combinedRange = FSharp.Compiler.Text.Range.unionRanges derefRange exprRange - let protocolRange = fcsRangeToLsp combinedRange - let! badString = getLineText lines protocolRange - let replacementString = badString.[1..] + ".Value" - return [ - { Title = "Use `.Value` instead of dereference operator" - File = codeActionParams.TextDocument - SourceDiagnostic = None - Kind = FixKind.Refactor - Edits = [| { Range = protocolRange - NewText = replacementString } |] } - ] - } diff --git a/src/FsAutoComplete/CodeFixes/ReplaceWithSuggestion.fs b/src/FsAutoComplete/CodeFixes/ReplaceWithSuggestion.fs new file mode 100644 index 000000000..86322b9e7 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ReplaceWithSuggestion.fs @@ -0,0 +1,30 @@ +module FsAutoComplete.CodeFix.ReplaceWithSuggestion + +open FsAutoComplete.CodeFix +open FsAutoComplete.CodeFix.Types +open FsToolkit.ErrorHandling +open FsAutoComplete +open FSharp.Compiler.Syntax + +let title suggestion = $"Replace with '%s{suggestion}'" +/// a codefix that replaces the use of an unknown identifier with a suggested identifier +let fix = + Run.ifDiagnosticByMessage + "Maybe you want one of the following:" + (fun diagnostic codeActionParams -> + diagnostic.Message.Split('\n').[1..] + |> Array.map + (fun suggestion -> + let suggestion = + suggestion.Trim() + |> PrettyNaming.AddBackticksToIdentifierIfNeeded + + { Edits = + [| { Range = diagnostic.Range + NewText = suggestion } |] + Title = title suggestion + File = codeActionParams.TextDocument + SourceDiagnostic = Some diagnostic + Kind = FixKind.Fix }) + |> Array.toList + |> AsyncResult.retn) diff --git a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs index 3d2012add..70694d5cd 100644 --- a/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs +++ b/src/FsAutoComplete/CodeFixes/ResolveNamespace.fs @@ -78,7 +78,7 @@ let fix (getParseResultsForFile: GetParseResultsForFile) (getNamespaceSuggestion let edits = [| yield insertLine docLine lineStr - if text.GetLineString(docLine + 1).Trim() <> "" then yield insertLine (docLine + 1) "" + if text.GetLineCount() < docLine + 1 && text.GetLineString(docLine + 1).Trim() <> "" then yield insertLine (docLine + 1) "" if (ctx.Pos.Column = 0 || ctx.ScopeKind = ScopeKind.Namespace) && docLine > 0 && not (text.GetLineString(docLine - 1).StartsWith "open") then diff --git a/src/FsAutoComplete/CodeFixes/SuggestedIdentifier.fs b/src/FsAutoComplete/CodeFixes/SuggestedIdentifier.fs deleted file mode 100644 index 896e191a4..000000000 --- a/src/FsAutoComplete/CodeFixes/SuggestedIdentifier.fs +++ /dev/null @@ -1,31 +0,0 @@ -module FsAutoComplete.CodeFix.SuggestedIdentifier - -open FsAutoComplete.CodeFix -open FsAutoComplete.CodeFix.Types -open FsToolkit.ErrorHandling -open FsAutoComplete - -/// a codefix that replaces the use of an unknown identifier with a suggested identifier -let fix = - Run.ifDiagnosticByMessage - "Maybe you want one of the following:" - (fun diagnostic codeActionParams -> - diagnostic.Message.Split('\n').[1..] - |> Array.map - (fun suggestion -> - let suggestion = suggestion.Trim() - - let suggestion = - if System.Text.RegularExpressions.Regex.IsMatch(suggestion, """^[a-zA-Z][a-zA-Z0-9']+$""") - then suggestion - else $"``%s{suggestion}``" - - { Edits = - [| { Range = diagnostic.Range - NewText = suggestion } |] - Title = $"Replace with %s{suggestion}" - File = codeActionParams.TextDocument - SourceDiagnostic = Some diagnostic - Kind = FixKind.Fix }) - |> Array.toList - |> AsyncResult.retn) diff --git a/src/FsAutoComplete/CodeFixes/UnusedOpens.fs b/src/FsAutoComplete/CodeFixes/UnusedOpens.fs deleted file mode 100644 index c0f42db19..000000000 --- a/src/FsAutoComplete/CodeFixes/UnusedOpens.fs +++ /dev/null @@ -1,28 +0,0 @@ -module FsAutoComplete.CodeFix.UnusedOpens - -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete.CodeFix -open FsAutoComplete.CodeFix.Types -open FsToolkit.ErrorHandling - -/// a codefix that removes unused open statements from the source -let fix : CodeFix = - Run.ifDiagnosticByMessage - "Unused open statement" - (fun d codeActionParams -> - let range = - { Start = - { Line = d.Range.Start.Line - 1 - Character = 1000 } - End = - { Line = d.Range.End.Line - Character = d.Range.End.Character } } - - let fix = - { Edits = [| { Range = range; NewText = "" } |] - File = codeActionParams.TextDocument - Title = "Remove unused open" - SourceDiagnostic = Some d - Kind = FixKind.Refactor } - - AsyncResult.retn [ fix ]) diff --git a/src/FsAutoComplete/CodeFixes/ChangeComparisonToMutableAssignment.fs b/src/FsAutoComplete/CodeFixes/UseMutationWhenValueIsMutable.fs similarity index 85% rename from src/FsAutoComplete/CodeFixes/ChangeComparisonToMutableAssignment.fs rename to src/FsAutoComplete/CodeFixes/UseMutationWhenValueIsMutable.fs index aee5896a3..893512d96 100644 --- a/src/FsAutoComplete/CodeFixes/ChangeComparisonToMutableAssignment.fs +++ b/src/FsAutoComplete/CodeFixes/UseMutationWhenValueIsMutable.fs @@ -1,4 +1,4 @@ -module FsAutoComplete.CodeFix.ChangeComparisonToMutableAssignment +module FsAutoComplete.CodeFix.UseMutationWhenValueIsMutable open FsToolkit.ErrorHandling open FsAutoComplete.CodeFix.Types @@ -8,6 +8,7 @@ open FsAutoComplete.CodeFix.Navigation open FsAutoComplete.LspHelpers open FSharp.Compiler.Symbols +let title = "Use '<-' to mutate value" /// a codefix that changes equality checking to mutable assignment when the compiler thinks it's relevant let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = Run.ifDiagnosticByCode @@ -38,15 +39,15 @@ let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix = match walkForwardUntilCondition lines endOfMutableValue (fun c -> c = '=') with | Some equalsPos -> - let! nextPos = inc lines equalsPos |> Result.ofOption (fun _ -> "next position wasn't valid") + let! prevPos = dec lines equalsPos |> Result.ofOption (fun _ -> "prev position wasn't valid") return [ { File = codeActionParams.TextDocument - Title = "Use '<-' to mutate value" + Title = title SourceDiagnostic = Some diagnostic Edits = [| { Range = - { Start = equalsPos - End = nextPos } + { Start = prevPos + End = equalsPos } NewText = "<-" } |] Kind = FixKind.Refactor } ] | None -> return [] diff --git a/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fs b/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fs new file mode 100644 index 000000000..674a223c4 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/WrapExpressionInParentheses.fs @@ -0,0 +1,27 @@ +module FsAutoComplete.CodeFix.WrapExpressionInParentheses + +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete + +let title = "Wrap expression in parentheses" +/// a codefix that parenthesizes a member expression that needs it +let fix (getRangeText: GetRangeText): CodeFix = + Run.ifDiagnosticByCode + (Set.ofList [ "597" ]) + (fun diagnostic codeActionParams -> + AsyncResult.retn [{ + Title = title + File = codeActionParams.TextDocument + SourceDiagnostic = Some diagnostic + Edits = + [| + { Range = { Start = diagnostic.Range.Start; End = diagnostic.Range.Start } + NewText = "(" } + { Range = { Start = diagnostic.Range.End; End = diagnostic.Range.End } + NewText = ")" } + |] + Kind = FixKind.Fix + }] + ) diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index d4b065c71..6534d8d62 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -843,14 +843,16 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS let getAbstractClassStubReplacements () = abstractClassStubReplacements () codeFixes <- - [| Run.ifEnabled (fun _ -> config.UnusedOpensAnalyzer) UnusedOpens.fix + [| Run.ifEnabled + (fun _ -> config.UnusedOpensAnalyzer) + (RemoveUnusedOpens.fix getFileLines) Run.ifEnabled (fun _ -> config.ResolveNamespaces) (ResolveNamespace.fix tryGetParseResultsForFile commands.GetNamespaceSuggestions) - SuggestedIdentifier.fix - RedundantQualifier.fix - Run.ifEnabled (fun _ -> config.UnusedDeclarationsAnalyzer) (UnusedValue.fix getRangeText) - NewWithDisposables.fix getRangeText + ReplaceWithSuggestion.fix + RemoveRedundantQualifier.fix + Run.ifEnabled (fun _ -> config.UnusedDeclarationsAnalyzer) (RenameUnusedValue.fix getRangeText) + AddNewKeywordToDisposableConstructorInvocation.fix getRangeText Run.ifEnabled (fun _ -> config.UnionCaseStubGeneration) (GenerateUnionCases.fix @@ -872,23 +874,23 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS tryGetParseResultsForFile commands.GetAbstractClassStub getAbstractClassStubReplacements) - MissingEquals.fix getFileLines - NegationToSubtraction.fix getFileLines - DoubleEqualsToSingleEquals.fix getRangeText - ColonInFieldType.fix - ParenthesizeExpression.fix getRangeText - RefCellAccesToNot.fix tryGetParseResultsForFile - UseSafeCastInsteadOfUnsafe.fix getRangeText + AddMissingEqualsToTypeDefinition.fix getFileLines + ChangePrefixNegationToInfixSubtraction.fix getFileLines + ConvertDoubleEqualsToSingleEquals.fix getRangeText + ChangeEqualsInFieldTypeToColon.fix + WrapExpressionInParentheses.fix getRangeText + ChangeRefCellDerefToNot.fix tryGetParseResultsForFile + ChangeDowncastToUpcast.fix getRangeText MakeDeclarationMutable.fix tryGetParseResultsForFile tryGetProjectOptions - ChangeComparisonToMutableAssignment.fix tryGetParseResultsForFile + UseMutationWhenValueIsMutable.fix tryGetParseResultsForFile ConvertInvalidRecordToAnonRecord.fix tryGetParseResultsForFile RemoveUnnecessaryReturnOrYield.fix tryGetParseResultsForFile getLineText - ChangeCSharpLambdaToFSharp.fix tryGetParseResultsForFile getLineText + ConvertCSharpLambdaToFSharpLambda.fix tryGetParseResultsForFile getLineText AddMissingFunKeyword.fix getFileLines getLineText MakeOuterBindingRecursive.fix tryGetParseResultsForFile getLineText AddMissingRecKeyword.fix getFileLines getLineText ConvertBangEqualsToInequality.fix getRangeText - ReplaceBangWithValueFunction.fix tryGetParseResultsForFile getLineText + ChangeDerefBangToValue.fix tryGetParseResultsForFile getLineText RemoveUnusedBinding.fix tryGetParseResultsForFile AddTypeToIndeterminateValue.fix tryGetParseResultsForFile tryGetProjectOptions ChangeTypeOfNameToNameOf.fix tryGetParseResultsForFile diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs index ef6b6efcc..f5d460afc 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs @@ -40,12 +40,652 @@ module CodeFix = ) codeActions + +let private addExplicitTypeToParameterTests state = + serverTestList (nameof AddExplicitTypeToParameter) state defaultConfigDto None (fun server -> [ + testCaseAsync "can suggest explicit parameter for record-typed function parameters" <| + CodeFix.check server + """ + type Foo = + { name: string } + + let name $0f = + f.name + """ + (Diagnostics.acceptAll) + (CodeFix.withTitle AddExplicitTypeToParameter.title) + """ + type Foo = + { name: string } + + let name (f: Foo) = + f.name + """ + ]) + +let private addMissingEqualsToTypeDefinitionTests state = + serverTestList (nameof AddMissingEqualsToTypeDefinition) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle AddMissingEqualsToTypeDefinition.title + testCaseAsync "can add = to record def" <| + CodeFix.check server + """ + type Person $0{ Name : string; Age : int; City : string } + """ + (Diagnostics.expectCode "3360") + selectCodeFix + """ + type Person = { Name : string; Age : int; City : string } + """ + testCaseAsync "can add = to union def" <| + CodeFix.check server + """ + type Name $0Name of string + """ + (Diagnostics.expectCode "3360") + selectCodeFix + """ + type Name = Name of string + """ + ]) + +let private addMissingFunKeywordTests state = + serverTestList (nameof AddMissingFunKeyword) state defaultConfigDto None (fun server -> [ + testCaseAsync "can generate the fun keyword when error 10 is raised" <| + CodeFix.check server + """ + let doThing = x $0-> printfn "%s" x + """ + (Diagnostics.expectCode "10") + (CodeFix.ofKind "quickfix" >> CodeFix.withTitle AddMissingFunKeyword.title) + """ + let doThing = fun x -> printfn "%s" x + """ + ]) + +let private addMissingInstanceMemberTests state = + serverTestList (nameof AddMissingInstanceMember) state defaultConfigDto None (fun server -> [ + testCaseAsync "can add this member prefix" <| + CodeFix.check server + """ + type C () = + member $0Foo() = () + """ + (Diagnostics.expectCode "673") + (CodeFix.ofKind "quickfix" >> CodeFix.withTitle AddMissingInstanceMember.title) + """ + type C () = + member x.Foo() = () + """ + ]) + +let private addMissingRecKeywordTests state = + serverTestList (nameof AddMissingRecKeyword) state defaultConfigDto None (fun server -> [ + // `rec` in single function is handled in `MakeOuterBindingRecursive` + testCaseAsync "can add rec to mutual recursive function" <| + CodeFix.check server + """ + $0let a x = x + and b x = x + """ + (Diagnostics.expectCode "576") + (CodeFix.withTitle (AddMissingRecKeyword.title "a")) + """ + let rec a x = x + and b x = x + """ + ]) + +let private addNewKeywordToDisposableConstructorInvocationTests state = + serverTestList (nameof AddNewKeywordToDisposableConstructorInvocation) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle AddNewKeywordToDisposableConstructorInvocation.title + testCaseAsync "can add new to Disposable" <| + CodeFix.check server + """ + open System.Threading.Tasks + let _ = $0Task(fun _ -> 1) + """ + (Diagnostics.expectCode "760") + selectCodeFix + """ + open System.Threading.Tasks + let _ = new Task(fun _ -> 1) + """ + testCaseAsync "can add new to Disposable with namespace" <| + CodeFix.check server + """ + let _ = System.Threading.Tasks.$0Task(fun _ -> 1) + """ + (Diagnostics.expectCode "760") + selectCodeFix + """ + let _ = new System.Threading.Tasks.Task(fun _ -> 1) + """ + testCaseAsync "doesn't trigger for not Disposable" <| + CodeFix.checkNotApplicable server + """ + let _ = System.$0String('.', 3) + """ + Diagnostics.acceptAll + selectCodeFix + ]) + +let private addTypeToIndeterminateValueTests state = + serverTestList (nameof AddTypeToIndeterminateValue) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle AddTypeToIndeterminateValue.title + testCaseAsync "can add type annotation to error 72 ('Lookup on object of indeterminate type')" <| + CodeFix.check server + """ + let data = [ + {| Name = "foo"; Value = 42 |} + {| Name = "bar"; Value = 13 |} + ] + let res = List.filter (fun d -> $0d.Value > 20) data + """ + (Diagnostics.expectCode "72") + selectCodeFix + """ + let data = [ + {| Name = "foo"; Value = 42 |} + {| Name = "bar"; Value = 13 |} + ] + let res = List.filter (fun (d: {| Name: string; Value: int |}) -> d.Value > 20) data + """ + testCaseAsync "can add type annotation to error 3245 ('The input to a copy-and-update expression that creates an anonymous record must be either an anonymous record or a record')" <| + CodeFix.check server + """ + [1..5] + |> List.fold + (fun s i -> + match i % 2 with + | 0 -> {| $0s with Evens = s.Evens + 1 |} + | _ -> s + ) + {| Evens = 0 |} + """ + (Diagnostics.expectCode "3245") + selectCodeFix + """ + [1..5] + |> List.fold + (fun (s: {| Evens: int |}) i -> + match i % 2 with + | 0 -> {| s with Evens = s.Evens + 1 |} + | _ -> s + ) + {| Evens = 0 |} + """ + ]) + +let private changeDerefBangToValueTests state = + serverTestList (nameof ChangeDerefBangToValue) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ChangeDerefBangToValue.title + testCaseAsync "can replace ! with .Value" <| + CodeFix.check server + """ + let rv = ref 5 + let v = $0!rv + """ + (Diagnostics.expectCode "3370") + selectCodeFix + """ + let rv = ref 5 + let v = rv.Value + """ + testCaseAsync "can replace ! with .Value when parens" <| + CodeFix.check server + """ + let rv = ref 5 + let v = $0!(rv) + """ + (Diagnostics.expectCode "3370") + selectCodeFix + """ + let rv = ref 5 + let v = (rv).Value + """ + testCaseAsync "can replace ! with .Value when function in parens" <| + CodeFix.check server + """ + let fr a = ref a + let v = $0!(fr 5) + """ + (Diagnostics.expectCode "3370") + selectCodeFix + """ + let fr a = ref a + let v = (fr 5).Value + """ + testCaseAsync "can replace ! with .Value when space between ! and variable" <| + CodeFix.check server + """ + let rv = ref 5 + let v = $0! rv + """ + (Diagnostics.expectCode "3370") + selectCodeFix + """ + let rv = ref 5 + let v = rv.Value + """ + testCaseAsync "can replace ! with .Value when when parens and space between ! and variable" <| + CodeFix.check server + """ + let rv = ref 5 + let v = $0! (rv) + """ + (Diagnostics.expectCode "3370") + selectCodeFix + """ + let rv = ref 5 + let v = (rv).Value + """ + ]) + +let private changeDowncastToUpcastTests state = + serverTestList (nameof ChangeDowncastToUpcast) state defaultConfigDto None (fun server -> [ + let selectOperatorCodeFix = CodeFix.withTitle ChangeDowncastToUpcast.titleUpcastOperator + let selectFunctionCodeFix = CodeFix.withTitle ChangeDowncastToUpcast.titleUpcastFunction + testCaseAsync "can change :?> to :>" <| + CodeFix.check server + """ + type I = interface end + type C() = interface I + + let v: I = C() $0:?> I + """ + (Diagnostics.expectCode "3198") + selectOperatorCodeFix + """ + type I = interface end + type C() = interface I + + let v: I = C() :> I + """ + testCaseAsync "can change downcast to upcast" <| + CodeFix.check server + """ + type I = interface end + type C() = interface I + + let v: I = $0downcast C() + """ + (Diagnostics.expectCode "3198") + selectFunctionCodeFix + """ + type I = interface end + type C() = interface I + + let v: I = upcast C() + """ + () + ]) + +let private changeEqualsInFieldTypeToColonTests state = + serverTestList (nameof ChangeEqualsInFieldTypeToColon) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ChangeEqualsInFieldTypeToColon.title + testCaseAsync "can change = to : in single line" <| + CodeFix.check server + """ + type A = { Name : string; Key $0= int } + """ + (Diagnostics.expectCode "10") + selectCodeFix + """ + type A = { Name : string; Key : int } + """ + testCaseAsync "can change = to : in multi line" <| + CodeFix.check server + """ + type A = { + Name : string + Key $0= int + } + """ + (Diagnostics.expectCode "10") + selectCodeFix + """ + type A = { + Name : string + Key : int + } + """ + ]) + +let private changePrefixNegationToInfixSubtractionTests state = + serverTestList (nameof ChangePrefixNegationToInfixSubtraction) state defaultConfigDto None (fun server -> [ + testCaseAsync "converts negation to subtraction" <| + CodeFix.check server + """ + let getListWithoutFirstAndLastElement list = + let l = List.length list + list[ 1 .. $0l -1 ] + """ + (Diagnostics.expectCode "3") + (CodeFix.ofKind "quickfix" >> CodeFix.withTitle ChangePrefixNegationToInfixSubtraction.title) + """ + let getListWithoutFirstAndLastElement list = + let l = List.length list + list[ 1 .. l - 1 ] + """ + ]) + +let private changeRefCellDerefToNotTests state = + serverTestList (nameof ChangeRefCellDerefToNot) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ChangeRefCellDerefToNot.title + testCaseAsync "can change simple deref to not" <| + CodeFix.check server + """ + let x = 1 + !$0x + """ + (Diagnostics.expectCode "1") + selectCodeFix + """ + let x = 1 + not x + """ + testCaseAsync "can change simple deref with parens to not" <| + CodeFix.check server + """ + let x = 1 + !($0x) + """ + (Diagnostics.expectCode "1") + selectCodeFix + """ + let x = 1 + not (x) + """ + testCaseAsync "can change deref of binary expr to not" <| + CodeFix.check server + """ + let x = 1 + !($0x = false) + """ + (Diagnostics.expectCode "1") + selectCodeFix + """ + let x = 1 + not (x = false) + """ + ]) + +let private changeTypeOfNameToNameOfTests state = + serverTestList (nameof ChangeTypeOfNameToNameOf) state defaultConfigDto None (fun server -> [ + testCaseAsync "can suggest fix" <| + CodeFix.check server + """ + let x = $0typeof>.Name + """ + (Diagnostics.acceptAll) + (CodeFix.ofKind "refactor" >> CodeFix.withTitle ChangeTypeOfNameToNameOf.title) + """ + let x = nameof(Async) + """ + ]) + +let private convertBangEqualsToInequalityTests state = + serverTestList (nameof ConvertBangEqualsToInequality) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ConvertBangEqualsToInequality.title + testCaseAsync "can change != to <>" <| + CodeFix.check server + """ + 1 $0!= 2 + """ + (Diagnostics.expectCode "43") + selectCodeFix + """ + 1 <> 2 + """ + ]) + +let private ConvertCSharpLambdaToFSharpLambdaTests state = + serverTestList (nameof ConvertCSharpLambdaToFSharpLambda) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ConvertCSharpLambdaToFSharpLambda.title + testCaseAsync "can convert csharp lambda in variable assignment with cursor on input" <| + CodeFix.check server + """ + let x = $0y => 1 + y + """ + Diagnostics.acceptAll + selectCodeFix + """ + let x = fun y -> 1 + y + """ + testCaseAsync "can convert csharp lambda in variable assignment with cursor on usage" <| + CodeFix.check server + """ + let x = y => 1 + $0y + """ + Diagnostics.acceptAll + selectCodeFix + """ + let x = fun y -> 1 + y + """ + //ENHANCEMENT: trigger on `=>` + // testCaseAsync "can convert csharp lambda in variable assignment with cursor on =>" <| + // CodeFix.check server + // """ + // let x = y $0=> 1 + y + // """ + // Diagnostics.acceptAll + // selectReplaceCSharpLambdaWithFSharp + // """ + // let x = fun y -> 1 + y + // """ + testCaseAsync "can convert csharp lambda in lambda with parens with cursor on input" <| + CodeFix.check server + """ + [1..10] |> List.map ($0x => 1 + x) + """ + Diagnostics.acceptAll + selectCodeFix + """ + [1..10] |> List.map (fun x -> 1 + x) + """ + testCaseAsync "can convert csharp lambda in lambda with parens with cursor on usage" <| + CodeFix.check server + """ + [1..10] |> List.map (x => 1 + $0x) + """ + Diagnostics.acceptAll + selectCodeFix + """ + [1..10] |> List.map (fun x -> 1 + x) + """ + testCaseAsync "keep multi-line lambda intact - cursor on input" <| + CodeFix.check server + """ + let x = + $0y => + let a = 1 + y + a + """ + Diagnostics.acceptAll + selectCodeFix + """ + let x = + fun y -> + let a = 1 + y + a + """ + testCaseAsync "keep multi-line lambda intact - cursor on usage" <| + CodeFix.check server + """ + let x = + y => + let a = 1 + $0y + a + """ + Diagnostics.acceptAll + selectCodeFix + """ + let x = + fun y -> + let a = 1 + y + a + """ + ]) + +let private convertDoubleEqualsToSingleEqualsTests state = + serverTestList (nameof ConvertDoubleEqualsToSingleEquals) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ConvertDoubleEqualsToSingleEquals.title + testCaseAsync "can replace == with =" <| + CodeFix.check server + """ + 1 $0== 1 + """ + (Diagnostics.expectCode "43") + selectCodeFix + """ + 1 = 1 + """ + testCaseAsync "doesn't replace existing operator == with =" <| + CodeFix.checkNotApplicable server + """ + let (==) a b = a = b + 1 $0== 1 + """ + Diagnostics.acceptAll + selectCodeFix + ]) + +let private convertInvalidRecordToAnonRecordTests state = + serverTestList (nameof ConvertInvalidRecordToAnonRecord) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ConvertInvalidRecordToAnonRecord.title + testCaseAsync "can convert single-line record with single field" <| + CodeFix.check server + """ + let v = { $0Name = "foo" } + """ + (Diagnostics.expectCode "39") + selectCodeFix + """ + let v = {| Name = "foo" |} + """ + testCaseAsync "can convert single-line record with two fields" <| + CodeFix.check server + """ + let v = { $0Name = "foo"; Value = 42 } + """ + (Diagnostics.expectCode "39") + selectCodeFix + """ + let v = {| Name = "foo"; Value = 42 |} + """ + testCaseAsync "can convert multi-line record with two fields" <| + CodeFix.check server + """ + let v = { + $0Name = "foo" + Value = 42 + } + """ + (Diagnostics.expectCode "39") + selectCodeFix + """ + let v = {| + Name = "foo" + Value = 42 + |} + """ + testCaseAsync "doesn't trigger for existing record" <| + CodeFix.checkNotApplicable server + """ + type V = { Name: string; Value: int } + let v = { $0Name = "foo"; Value = 42 } + """ + (Diagnostics.acceptAll) + selectCodeFix + testCaseAsync "doesn't trigger for anon record" <| + CodeFix.checkNotApplicable server + """ + let v = {| $0Name = "foo"; Value = 42 |} + """ + (Diagnostics.acceptAll) + selectCodeFix + ]) + +let private convertPositionalDUToNamedTests state = + serverTestList (nameof ConvertPositionalDUToNamed) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle ConvertPositionalDUToNamed.title + testCaseAsync "in parenthesized let binding" <| + CodeFix.check server + """ + type A = A of a: int * b: bool + + let (A(a$0, b)) = A(1, true) + """ + Diagnostics.acceptAll + selectCodeFix + """ + type A = A of a: int * b: bool + + let (A(a = a; b = b;)) = A(1, true) + """ + testCaseAsync "in simple match" <| + CodeFix.check server + """ + type A = A of a: int * b: bool + + match A(1, true) with + | A(a$0, b) -> () + """ + Diagnostics.acceptAll + selectCodeFix + """ + type A = A of a: int * b: bool + + match A(1, true) with + | A(a = a; b = b;) -> () + """ + testCaseAsync "in parenthesized match" <| + CodeFix.check server + """ + type A = A of a: int * b: bool + + match A(1, true) with + | (A(a$0, b)) -> () + """ + Diagnostics.acceptAll + selectCodeFix + """ + type A = A of a: int * b: bool + + match A(1, true) with + | (A(a = a; b = b;)) -> () + """ + testCaseAsync "when there is one new field on the DU" <| + CodeFix.check server + """ + type ThirdFieldWasJustAdded = ThirdFieldWasJustAdded of a: int * b: bool * c: char + + let (ThirdFieldWasJustAdded($0a, b)) = ThirdFieldWasJustAdded(1, true, 'c') + """ + Diagnostics.acceptAll + selectCodeFix + """ + type ThirdFieldWasJustAdded = ThirdFieldWasJustAdded of a: int * b: bool * c: char + + let (ThirdFieldWasJustAdded(a = a; b = b; c = _;)) = ThirdFieldWasJustAdded(1, true, 'c') + """ + testCaseAsync "when there are multiple new fields on the DU" <| + CodeFix.check server + """ + type U = U of aValue: int * boolean: int * char: char * dec: decimal * element: int + let (U($0a, b)) = failwith "..." + """ + Diagnostics.acceptAll + selectCodeFix + """ + type U = U of aValue: int * boolean: int * char: char * dec: decimal * element: int + let (U(aValue = a; boolean = b; char = _; dec = _; element = _;)) = failwith "..." + """ + ]) + let private generateAbstractClassStubTests state = let config = { defaultConfigDto with AbstractClassStubGeneration = Some true } - // issue: returns same fix twice: - // Once for error 54 (`This type is 'abstract' since some abstract members have not been given an implementation.`) - // And once for error 365 (`No implementation was given for those members [...]`) - pserverTestList (nameof GenerateAbstractClassStub) state config None (fun server -> [ + serverTestList (nameof GenerateAbstractClassStub) state config None (fun server -> [ let selectCodeFix = CodeFix.withTitle GenerateAbstractClassStub.title testCaseAsync "can generate a derivative of a long ident - System.IO.Stream" <| CodeFix.checkApplicable server @@ -64,37 +704,168 @@ let private generateAbstractClassStubTests state = """ (Diagnostics.expectCode "365") selectCodeFix - ]) + ptestCaseAsync "can generate abstract class stub" <| + // issue: Wants to insert text in line 13, column 12. + // But Line 13 (line with `"""`) is empty -> no column 12 + CodeFix.check server + """ + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 -let private generateUnionCasesTests state = - let config = - { defaultConfigDto with - UnionCaseStubGeneration = Some true - UnionCaseStubGenerationBody = Some "failwith \"---\"" - } - serverTestList (nameof GenerateUnionCases) state config None (fun server -> [ - let selectCodeFix = CodeFix.withTitle GenerateUnionCases.title - testCaseAsync "can generate match cases for a simple DU" <| + abstract Name : string with get + abstract Area : float with get + + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type $0Square(x,y, sideLength) = + inherit Shape(x,y) + """ + (Diagnostics.expectCode "365") + selectCodeFix + """ + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 + + abstract Name : string with get + abstract Area : float with get + + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type Square(x,y, sideLength) = + inherit Shape(x,y) + + override this.Area: float = + failwith "Not Implemented" + override this.Name: string = + failwith "Not Implemented" + """ + ptestCaseAsync "can generate abstract class stub without trailing nl" <| + // issue: Wants to insert text in line 13, column 12. + // But there's no line 13 (last line is line 12) CodeFix.check server """ - type Letter = A | B | C + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 - let char = A + abstract Name : string with get + abstract Area : float with get - match $0char with - | A -> () + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type $0Square(x,y, sideLength) = + inherit Shape(x,y)""" + (Diagnostics.expectCode "365") + selectCodeFix """ - (Diagnostics.expectCode "25") - (CodeFix.withTitle GenerateUnionCases.title) + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 + + abstract Name : string with get + abstract Area : float with get + + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type Square(x,y, sideLength) = + inherit Shape(x,y) + + override this.Area: float = + failwith "Not Implemented" + override this.Name: string = + failwith "Not Implemented" """ - type Letter = A | B | C + ptestCaseAsync "inserts override in correct place" <| + // issue: inserts overrides after `let a = ...`, not before + CodeFix.check server + """ + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 - let char = A + abstract Name : string with get + abstract Area : float with get - match char with - | A -> () - | B -> failwith "---" - | C -> failwith "---" + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type $0Square(x,y, sideLength) = + inherit Shape(x,y) + let a = 0 + """ + (Diagnostics.expectCode "365") + selectCodeFix + """ + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 + + abstract Name : string with get + abstract Area : float with get + + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type Square(x,y, sideLength) = + inherit Shape(x,y) + + override this.Area: float = + failwith "Not Implemented" + override this.Name: string = + failwith "Not Implemented" + let a = 0 + """ + ptestCaseAsync "can generate abstract class stub with existing override" <| + // issue: Generates override for already existing member + CodeFix.check server + """ + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 + + abstract Name : string with get + abstract Area : float with get + + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type $0Square(x,y, sideLength) = + inherit Shape(x,y) + """ + (Diagnostics.expectCode "365") + selectCodeFix + """ + [] + type Shape(x0: float, y0: float) = + let mutable x, y = x0, y0 + + abstract Name : string with get + abstract Area : float with get + + member _.Move dx dy = + x <- x + dx + y <- y + dy + + type Square(x,y, sideLength) = + inherit Shape(x,y) + + override this.Name = "Circle" + + override this.Area: float = + failwith "Not Implemented" """ ]) @@ -122,130 +893,221 @@ let private generateRecordStubTests state = """ ]) -let private addMissingFunKeywordTests state = - serverTestList (nameof AddMissingFunKeyword) state defaultConfigDto None (fun server -> [ - testCaseAsync "can generate the fun keyword when error 10 is raised" <| +let private generateUnionCasesTests state = + let config = + { defaultConfigDto with + UnionCaseStubGeneration = Some true + UnionCaseStubGenerationBody = Some "failwith \"---\"" + } + serverTestList (nameof GenerateUnionCases) state config None (fun server -> [ + let selectCodeFix = CodeFix.withTitle GenerateUnionCases.title + testCaseAsync "can generate match cases for a simple DU" <| CodeFix.check server """ - let doThing = x $0-> printfn "%s" x + type Letter = A | B | C + + let char = A + + match $0char with + | A -> () """ - (Diagnostics.expectCode "10") - (CodeFix.ofKind "quickfix" >> CodeFix.withTitle AddMissingFunKeyword.title) + (Diagnostics.expectCode "25") + (CodeFix.withTitle GenerateUnionCases.title) """ - let doThing = fun x -> printfn "%s" x + type Letter = A | B | C + + let char = A + + match char with + | A -> () + | B -> failwith "---" + | C -> failwith "---" """ ]) -let private makeOuterBindingRecursiveTests state = - serverTestList (nameof MakeOuterBindingRecursive) state defaultConfigDto None (fun server -> [ - testCaseAsync "can make the outer binding recursive when self-referential" <| +let private makeDeclarationMutableTests state = + serverTestList (nameof MakeDeclarationMutable) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle MakeDeclarationMutable.title + testCaseAsync "can make decl mutable in top level assignment" <| CodeFix.check server """ - let mySum xs acc = - match xs with - | [] -> acc - | _ :: tail -> - $0mySum tail (acc + 1) + let x = 0 + x $0<- 1 """ - (Diagnostics.expectCode "39") - (CodeFix.ofKind "quickfix" >> CodeFix.withTitle MakeOuterBindingRecursive.title) + (Diagnostics.expectCode "27") + selectCodeFix """ - let rec mySum xs acc = - match xs with - | [] -> acc - | _ :: tail -> - mySum tail (acc + 1) + let mutable x = 0 + x <- 1 """ - ]) - -let private changeTypeOfNameToNameOfTests state = - serverTestList (nameof ChangeTypeOfNameToNameOf) state defaultConfigDto None (fun server -> [ - testCaseAsync "can suggest fix" <| + testCaseAsync "can make decl mutable in nested assignment" <| CodeFix.check server """ - let x = $0typeof>.Name + let x = 0 + let _ = + x $0<- 1 + () """ - (Diagnostics.acceptAll) - (CodeFix.ofKind "refactor" >> CodeFix.withTitle ChangeTypeOfNameToNameOf.title) + (Diagnostics.expectCode "27") + selectCodeFix """ - let x = nameof(Async) + let mutable x = 0 + let _ = + x <- 1 + () + """ + testCaseAsync "can make decl mutable in function" <| + CodeFix.check server + """ + let count xs = + let counter = 0 + for x in xs do + counter $0<- counter + 1 + counter + """ + (Diagnostics.expectCode "27") + selectCodeFix + """ + let count xs = + let mutable counter = 0 + for x in xs do + counter <- counter + 1 + counter + """ + testCaseAsync "doesn't trigger for already mutable variable" <| + CodeFix.checkNotApplicable server + """ + let mutable x = 0 + x $0<- 1 + """ + Diagnostics.acceptAll + selectCodeFix + testCaseAsync "doesn't trigger for immutable parameter" <| + CodeFix.checkNotApplicable server + """ + let f (v: int) = + v $0<- 1 + v + """ + Diagnostics.acceptAll + selectCodeFix + testCaseAsync "doesn't trigger for immutable member parameter" <| + CodeFix.checkNotApplicable server + """ + type C() = + member _.M(v: int) + v $0<- 1 """ + Diagnostics.acceptAll + selectCodeFix ]) -let private addMissingInstanceMemberTests state = - serverTestList (nameof AddMissingInstanceMember) state defaultConfigDto None (fun server -> [ - testCaseAsync "can add this member prefix" <| +let private makeOuterBindingRecursiveTests state = + serverTestList (nameof MakeOuterBindingRecursive) state defaultConfigDto None (fun server -> [ + testCaseAsync "can make the outer binding recursive when self-referential" <| CodeFix.check server """ - type C () = - member $0Foo() = () + let mySum xs acc = + match xs with + | [] -> acc + | _ :: tail -> + $0mySum tail (acc + 1) """ - (Diagnostics.expectCode "673") - (CodeFix.ofKind "quickfix" >> CodeFix.withTitle AddMissingInstanceMember.title) + (Diagnostics.expectCode "39") + (CodeFix.ofKind "quickfix" >> CodeFix.withTitle MakeOuterBindingRecursive.title) """ - type C () = - member x.Foo() = () + let rec mySum xs acc = + match xs with + | [] -> acc + | _ :: tail -> + mySum tail (acc + 1) """ ]) -let private unusedValueTests state = - let config = { defaultConfigDto with UnusedDeclarationsAnalyzer = Some true } - serverTestList (nameof UnusedValue) state config None (fun server -> [ - let selectReplace = CodeFix.ofKind "refactor" >> CodeFix.withTitle UnusedValue.titleReplace - let selectPrefix = CodeFix.ofKind "refactor" >> CodeFix.withTitle UnusedValue.titlePrefix - - testCaseAsync "can replace unused self-reference" <| +let private removeRedundantQualifierTests state = + let config = { defaultConfigDto with SimplifyNameAnalyzer = Some true } + serverTestList (nameof RemoveRedundantQualifier) state config None (fun server -> [ + let selectCodeFix = CodeFix.withTitle RemoveRedundantQualifier.title + testCaseAsync "can remove redundant namespace" <| CodeFix.check server """ - type MyClass() = - member $0this.DoAThing() = () + open System + let _ = $0System.String.IsNullOrWhiteSpace "foo" """ - (Diagnostics.acceptAll) - selectReplace + Diagnostics.acceptAll + selectCodeFix """ - type MyClass() = - member _.DoAThing() = () + open System + let _ = String.IsNullOrWhiteSpace "foo" """ - testCaseAsync "can replace unused binding" <| + testCaseAsync "doesn't remove necessary namespace" <| + CodeFix.checkNotApplicable server + """ + let _ = $0System.String.IsNullOrWhiteSpace "foo" + """ + Diagnostics.acceptAll + selectCodeFix + ]) + +let private removeUnnecessaryReturnOrYieldTests state = + serverTestList (nameof RemoveUnnecessaryReturnOrYield) state defaultConfigDto None (fun server -> [ + testCaseAsync "can remove return" <| CodeFix.check server """ - let $0six = 6 + let f x = + $0return x """ - (Diagnostics.acceptAll) - selectReplace + (Diagnostics.expectCode "748") + (CodeFix.withTitle (RemoveUnnecessaryReturnOrYield.title "return")) """ - let _ = 6 + let f x = + x """ - testCaseAsync "can prefix unused binding" <| + testCaseAsync "can remove return!" <| CodeFix.check server """ - let $0six = 6 + let f x = + $0return! x """ - (Diagnostics.acceptAll) - selectPrefix + (Diagnostics.expectCode "748") + (CodeFix.withTitle (RemoveUnnecessaryReturnOrYield.title "return!")) """ - let _six = 6 + let f x = + x """ - testCaseAsync "can replace unused parameter" <| + testCaseAsync "can remove yield" <| CodeFix.check server """ - let add one two $0three = one + two + let f x = + $0yield x """ - (Diagnostics.acceptAll) - selectReplace + (Diagnostics.expectCode "747") + (CodeFix.withTitle (RemoveUnnecessaryReturnOrYield.title "yield")) """ - let add one two _ = one + two + let f x = + x """ - testCaseAsync "can prefix unused parameter" <| + testCaseAsync "can remove yield!" <| CodeFix.check server """ - let add one two $0three = one + two + let f x = + $0yield! x """ - (Diagnostics.log >> Diagnostics.acceptAll) - (CodeFix.log >> selectPrefix) + (Diagnostics.expectCode "747") + (CodeFix.withTitle (RemoveUnnecessaryReturnOrYield.title "yield!")) """ - let add one two _three = one + two + let f x = + x + """ + testCaseAsync "doesn't trigger in seq" <| + CodeFix.checkNotApplicable server + """ + let f x = seq { + $0yield x + } """ + (Diagnostics.acceptAll) + (CodeFix.withTitle (RemoveUnnecessaryReturnOrYield.title "yield")) ]) let private removeUnusedBindingTests state = @@ -292,110 +1154,349 @@ let private removeUnusedBindingTests state = """ ]) -let private addExplicitTypeToParameterTests state = - serverTestList (nameof AddExplicitTypeToParameter) state defaultConfigDto None (fun server -> [ - testCaseAsync "can suggest explicit parameter for record-typed function parameters" <| +let private removeUnusedOpensTests state = + let config = { defaultConfigDto with UnusedOpensAnalyzer = Some true } + serverTestList (nameof RemoveUnusedOpens) state config None (fun server -> [ + let selectCodeFix = CodeFix.withTitle RemoveUnusedOpens.title + testCaseAsync "can remove single unused open" <| CodeFix.check server """ - type Foo = - { name: string } - - let name $0f = - f.name + open $0System """ - (Diagnostics.acceptAll) - (CodeFix.withTitle AddExplicitTypeToParameter.title) + Diagnostics.acceptAll + selectCodeFix + "" + testCaseAsync "removes just current unused open" <| + // unlike VS, `RemoveUnusedOpens` removes just current open (with cursor) and not all unused opens + CodeFix.check server """ - type Foo = - { name: string } + open $0System + open System.Text + """ + Diagnostics.acceptAll + selectCodeFix + """ + open System.Text + """ + testCaseAsync "removes just current unused open 2" <| + CodeFix.check server + """ + open System + open $0System.Text + """ + Diagnostics.acceptAll + selectCodeFix + """ + open System + """ + testCaseAsync "doesn't remove used open" <| + CodeFix.checkNotApplicable server + """ + open $0System - let name (f: Foo) = - f.name + let _ = String.IsNullOrWhiteSpace "" + """ + Diagnostics.acceptAll + selectCodeFix + testCaseAsync "can remove open in nested module" <| + CodeFix.check server + """ + module A = + module B = + open $0System + () + () + """ + Diagnostics.acceptAll + selectCodeFix + """ + module A = + module B = + () + () + """ + testCaseAsync "can remove used open in nested module when outer scope opens same open" <| + CodeFix.check server + """ + open System + module A = + module B = + open $0System + let x = String.IsNullOrWhiteSpace "" + () + () + """ + Diagnostics.acceptAll + selectCodeFix + """ + open System + module A = + module B = + let x = String.IsNullOrWhiteSpace "" + () + () """ + //ENHANCEMENT: detect open in outer scope as unused too + // testCaseAsync "can remove used open in outer scope when usage in nested scope has own open" <| + // CodeFix.check server + // """ + // open $0System + // module A = + // module B = + // open System + // let x = String.IsNullOrWhiteSpace "" + // () + // () + // """ + // Diagnostics.acceptAll + // selectCodeFix + // """ + // module A = + // module B = + // open System + // let x = String.IsNullOrWhiteSpace "" + // () + // () + // """ + testCaseAsync "doesn't trigger for used open" <| + CodeFix.checkNotApplicable server + """ + open $0System + let x = String.IsNullOrWhiteSpace "" + """ + Diagnostics.acceptAll + selectCodeFix ]) -let private negationToSubtractionTests state = - serverTestList (nameof NegationToSubtraction) state defaultConfigDto None (fun server -> [ - testCaseAsync "converts negation to subtraction" <| +let private replaceWithSuggestionTests state = + serverTestList (nameof ReplaceWithSuggestion) state defaultConfigDto None (fun server -> [ + let selectCodeFix replacement = CodeFix.withTitle (ReplaceWithSuggestion.title replacement) + testCaseAsync "can change Min to min" <| CodeFix.check server """ - let getListWithoutFirstAndLastElement list = - let l = List.length list - list[ 1 .. $0l -1 ] + let x = $0Min(2.0, 1.0) + """ + Diagnostics.acceptAll + (selectCodeFix "min") + """ + let x = min(2.0, 1.0) + """ + testSequenced <| testList "can get multiple suggestions for flout" [ + testCaseAsync "can change flout to float" <| + CodeFix.check server + """ + let x = $0flout 2 + """ + Diagnostics.acceptAll + (selectCodeFix "float") + """ + let x = float 2 + """ + testCaseAsync "can change flout to float32" <| + CodeFix.check server + """ + let x = $0flout 2 + """ + Diagnostics.acceptAll + (selectCodeFix "float32") + """ + let x = float32 2 + """ + ] + testCaseAsync "can change flout to float in var type" <| + CodeFix.check server """ - (Diagnostics.expectCode "3") - (CodeFix.ofKind "quickfix" >> CodeFix.withTitle NegationToSubtraction.title) + let x: $0flout = 2.0 """ - let getListWithoutFirstAndLastElement list = - let l = List.length list - list[ 1 .. l - 1 ] + Diagnostics.acceptAll + (selectCodeFix "float") + """ + let x: float = 2.0 + """ + testCaseAsync "can change namespace in open" <| + CodeFix.check server + """ + open System.Text.$0RegularEcpressions + """ + Diagnostics.acceptAll + (selectCodeFix "RegularExpressions") + """ + open System.Text.RegularExpressions + """ + testCaseAsync "can change type in type constructor" <| + CodeFix.check server + """ + open System.Text.RegularExpressions + let x = $0Regec() + """ + Diagnostics.acceptAll + (selectCodeFix "Regex") + """ + open System.Text.RegularExpressions + let x = Regex() + """ + testCaseAsync "can replace identifier in double-backticks" <| + CodeFix.check server + """ + let ``hello world`` = 2 + let x = ``$0hello word`` + """ + Diagnostics.acceptAll + (selectCodeFix "``hello world``") + """ + let ``hello world`` = 2 + let x = ``hello world`` + """ + testCaseAsync "can add double-backticks" <| + CodeFix.check server + """ + let ``hello world`` = 2 + let x = $0helloword + """ + Diagnostics.acceptAll + (selectCodeFix "``hello world``") + """ + let ``hello world`` = 2 + let x = ``hello world`` """ ]) -let private convertPositionalDUToNamedTests state = - serverTestList (nameof ConvertPositionalDUToNamed) state defaultConfigDto None (fun server -> [ - let selectCodeFix = CodeFix.withTitle ConvertPositionalDUToNamed.title - testCaseAsync "in parenthesized let binding" <| - CodeFix.check server +let private resolveNamespaceTests state = + let config = { defaultConfigDto with ResolveNamespaces = Some true } + serverTestList (nameof ResolveNamespace) state config None (fun server -> [ + testCaseAsync "doesn't fail when target not in last line" <| + CodeFix.checkApplicable server """ - type A = A of a: int * b: bool + let x = $0Min(2.0, 1.0) + """ // Note: new line at end! + (Diagnostics.log >> Diagnostics.acceptAll) + (CodeFix.log >> CodeFix.matching (fun ca -> ca.Title.StartsWith "open") >> Array.take 1) + testCaseAsync "doesn't fail when target in last line" <| + CodeFix.checkApplicable server + "let x = $0Min(2.0, 1.0)" // Note: No new line at end! + (Diagnostics.log >> Diagnostics.acceptAll) + (CodeFix.log >> CodeFix.matching (fun ca -> ca.Title.StartsWith "open") >> Array.take 1) - let (A(a$0, b)) = A(1, true) + //TODO: Implement & unify with `Completion.AutoOpen` (`CompletionTests.fs`) + // Issues: + // * Complex because of nesting modules (-> where to open) + // * Different open locations of CodeFix and AutoOpen + ]) + +let private renameUnusedValue state = + let config = { defaultConfigDto with UnusedDeclarationsAnalyzer = Some true } + serverTestList (nameof RenameUnusedValue) state config None (fun server -> [ + let selectReplace = CodeFix.ofKind "refactor" >> CodeFix.withTitle RenameUnusedValue.titleReplace + let selectPrefix = CodeFix.ofKind "refactor" >> CodeFix.withTitle RenameUnusedValue.titlePrefix + + testCaseAsync "can replace unused self-reference" <| + CodeFix.check server """ - Diagnostics.acceptAll - selectCodeFix + type MyClass() = + member $0this.DoAThing() = () """ - type A = A of a: int * b: bool - - let (A(a = a; b = b;)) = A(1, true) + (Diagnostics.acceptAll) + selectReplace """ - testCaseAsync "in simple match" <| + type MyClass() = + member _.DoAThing() = () + """ + testCaseAsync "can replace unused binding" <| CodeFix.check server """ - type A = A of a: int * b: bool - - match A(1, true) with - | A(a$0, b) -> () + let $0six = 6 """ - Diagnostics.acceptAll - selectCodeFix + (Diagnostics.acceptAll) + selectReplace """ - type A = A of a: int * b: bool - - match A(1, true) with - | A(a = a; b = b;) -> () + let _ = 6 """ - testCaseAsync "in parenthesized match" <| + testCaseAsync "can prefix unused binding" <| CodeFix.check server """ - type A = A of a: int * b: bool + let $0six = 6 + """ + (Diagnostics.acceptAll) + selectPrefix + """ + let _six = 6 + """ + testCaseAsync "can replace unused parameter" <| + CodeFix.check server + """ + let add one two $0three = one + two + """ + (Diagnostics.acceptAll) + selectReplace + """ + let add one two _ = one + two + """ + testCaseAsync "can prefix unused parameter" <| + CodeFix.check server + """ + let add one two $0three = one + two + """ + (Diagnostics.log >> Diagnostics.acceptAll) + (CodeFix.log >> selectPrefix) + """ + let add one two _three = one + two + """ + ]) - match A(1, true) with - | (A(a$0, b)) -> () +let private useMutationWhenValueIsMutableTests state = + serverTestList (nameof UseMutationWhenValueIsMutable) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle UseMutationWhenValueIsMutable.title + testCaseAsync "can replace = with <- when cursor on =" <| + CodeFix.check server """ - Diagnostics.acceptAll + let _ = + let mutable v = 42 + v $0= 5 + v + """ + (Diagnostics.expectCode "20") selectCodeFix """ - type A = A of a: int * b: bool - - match A(1, true) with - | (A(a = a; b = b;)) -> () + let _ = + let mutable v = 42 + v <- 5 + v """ - testCaseAsync "when there are new fields on the DU" <| - //ENHANCEMENT: add space before wildcard case + testCaseAsync "can replace = with <- when cursor on variable" <| CodeFix.check server """ - type ThirdFieldWasJustAdded = ThirdFieldWasJustAdded of a: int * b: bool * c: char - - let (ThirdFieldWasJustAdded($0a, b)) = ThirdFieldWasJustAdded(1, true, 'c') + let _ = + let mutable v = 42 + $0v = 5 + v + """ + (Diagnostics.expectCode "20") + selectCodeFix + """ + let _ = + let mutable v = 42 + v <- 5 + v + """ + testCaseAsync "doesn't suggest fix when = is comparison" <| + CodeFix.checkNotApplicable server + """ + let _ = + let mutable v = 42 + v $0= 5 """ Diagnostics.acceptAll selectCodeFix + testCaseAsync "doesn't suggest fix when variable is not mutable" <| + CodeFix.checkNotApplicable server """ - type ThirdFieldWasJustAdded = ThirdFieldWasJustAdded of a: int * b: bool * c: char - - let (ThirdFieldWasJustAdded(a = a; b = b;c = _;)) = ThirdFieldWasJustAdded(1, true, 'c') + let _ = + let v = 42 + v $0= 5 + v """ + Diagnostics.acceptAll + selectCodeFix ]) let private useTripleQuotedInterpolationTests state = @@ -413,18 +1514,224 @@ let private useTripleQuotedInterpolationTests state = " ]) +let private wrapExpressionInParenthesesTests state = + serverTestList (nameof WrapExpressionInParentheses) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle WrapExpressionInParentheses.title + testCaseAsync "can add parenthesize expression" <| + CodeFix.check server + """ + printfn "%b" System.String.$0IsNullOrWhiteSpace("foo") + """ + (Diagnostics.expectCode "597") + selectCodeFix + """ + printfn "%b" (System.String.IsNullOrWhiteSpace("foo")) + """ + testCaseAsync "doesn't trigger for expression in parens" <| + CodeFix.checkNotApplicable server + """ + printfn "%b" (System.String.$0IsNullOrWhiteSpace("foo")) + """ + Diagnostics.acceptAll + selectCodeFix + ]) + +/// Helper functions for CodeFixes +module private CodeFixHelpers = + // `src\FsAutoComplete\CodeFixes.fs` -> `FsAutoComplete.CodeFix` + open Navigation + open FSharp.Compiler.Text + open Utils.TextEdit + + let private navigationTests = + testList (nameof Navigation) [ + let extractTwoCursors text = + let (text, poss) = Cursors.extract text + let text = SourceText.ofString text + (text, (poss[0], poss[1])) + + testList (nameof tryEndOfPrevLine) [ + testCase "can get end of prev line when not border line" <| fun _ -> + let text = """let foo = 4 +let bar = 5 +let baz = 5$0 +let $0x = 5 +let y = 7 +let z = 4""" + let (text, (expected, current)) = text |> extractTwoCursors + let actual = tryEndOfPrevLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "can get end of prev line when last line" <| fun _ -> + let text = """let foo = 4 +let bar = 5 +let baz = 5 +let x = 5 +let y = 7$0 +let z$0 = 4""" + let (text, (expected, current)) = text |> extractTwoCursors + let actual = tryEndOfPrevLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "cannot get end of prev line when first line" <| fun _ -> + let text = """let $0foo$0 = 4 +let bar = 5 +let baz = 5 +let x = 5 +let y = 7 +let z = 4""" + let (text, (_, current)) = text |> extractTwoCursors + let actual = tryEndOfPrevLine text current.Line + Expect.isNone actual "No prev line in first line" + + testCase "cannot get end of prev line when single line" <| fun _ -> + let text = SourceText.ofString "let foo = 4" + let line = 0 + let actual = tryEndOfPrevLine text line + Expect.isNone actual "No prev line in first line" + ] + testList (nameof tryStartOfNextLine) [ + // this would be WAY easier by just using `{ Line = current.Line + 1; Character = 0 }`... + testCase "can get start of next line when not border line" <| fun _ -> + let text = """let foo = 4 +let bar = 5 +let baz = 5 +let $0x = 5 +$0let y = 7 +let z = 4""" + let (text, (current, expected)) = text |> extractTwoCursors + let actual = tryStartOfNextLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "can get start of next line when first line" <| fun _ -> + let text = """let $0foo = 4 +$0let bar = 5 +let baz = 5 +let x = 5 +let y = 7 +let z = 4""" + let (text, (current, expected)) = text |> extractTwoCursors + let actual = tryStartOfNextLine text current.Line + Expect.equal actual (Some expected) "Incorrect pos" + + testCase "cannot get start of next line when last line" <| fun _ -> + let text = """let foo = 4 +let bar = 5 +let baz = 5 +let x = 5 +let y = 7 +let $0z$0 = 4""" + let (text, (current, _)) = text |> extractTwoCursors + let actual = tryStartOfNextLine text current.Line + Expect.isNone actual "No next line in last line" + + testCase "cannot get start of next line when single line" <| fun _ -> + let text = SourceText.ofString "let foo = 4" + let line = 0 + let actual = tryStartOfNextLine text line + Expect.isNone actual "No next line in first line" + ] + testList (nameof rangeToDeleteFullLine) [ + testCase "can get all range for single line" <| fun _ -> + let text = "$0let foo = 4$0" + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } + + let line = fin.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + + testCase "can get line range with leading linebreak in not border line" <| fun _ -> + let text = """let foo = 4 +let bar = 5 +let baz = 5$0 +let x = 5$0 +let y = 7 +let z = 4""" + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } + + let line = fin.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + + testCase "can get line range with leading linebreak in last line" <| fun _ -> + let text = """let foo = 4 +let bar = 5 +let baz = 5 +let x = 5 +let y = 7$0 +let z = 4$0""" + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } + + let line = fin.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + + testCase "can get line range with trailing linebreak in first line" <| fun _ -> + let text = """$0let foo = 4 +$0let bar = 5 +let baz = 5 +let x = 5 +let y = 7 +let z = 4""" + let (text, (start, fin)) = text |> extractTwoCursors + let expected = { Start = start; End = fin } + + let line = start.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + + testCase "can get all range for single empty line" <| fun _ -> + let text = SourceText.ofString "" + let pos = { Line = 0; Character = 0 } + let expected = { Start = pos; End = pos } + + let line = pos.Line + let actual = text |> rangeToDeleteFullLine line + Expect.equal actual expected "Incorrect range" + ] + ] + + let tests = testList ($"{nameof(FsAutoComplete)}.{nameof FsAutoComplete.CodeFix}") [ + navigationTests + ] + let tests state = testList "CodeFix tests" [ + CodeFixHelpers.tests + + addExplicitTypeToParameterTests state + addMissingEqualsToTypeDefinitionTests state + addMissingFunKeywordTests state + addMissingInstanceMemberTests state + addMissingRecKeywordTests state + addNewKeywordToDisposableConstructorInvocationTests state + addTypeToIndeterminateValueTests state + changeDerefBangToValueTests state + changeDowncastToUpcastTests state + changeEqualsInFieldTypeToColonTests state + changePrefixNegationToInfixSubtractionTests state + changeRefCellDerefToNotTests state + changeTypeOfNameToNameOfTests state + convertBangEqualsToInequalityTests state + ConvertCSharpLambdaToFSharpLambdaTests state + convertDoubleEqualsToSingleEqualsTests state + convertInvalidRecordToAnonRecordTests state + convertPositionalDUToNamedTests state generateAbstractClassStubTests state - generateUnionCasesTests state generateRecordStubTests state - addMissingFunKeywordTests state + generateUnionCasesTests state + makeDeclarationMutableTests state makeOuterBindingRecursiveTests state - changeTypeOfNameToNameOfTests state - addMissingInstanceMemberTests state - unusedValueTests state + removeRedundantQualifierTests state + removeUnnecessaryReturnOrYieldTests state removeUnusedBindingTests state - addExplicitTypeToParameterTests state - negationToSubtractionTests state - convertPositionalDUToNamedTests state + removeUnusedOpensTests state + renameUnusedValue state + replaceWithSuggestionTests state + resolveNamespaceTests state + useMutationWhenValueIsMutableTests state useTripleQuotedInterpolationTests state + wrapExpressionInParenthesesTests state ] diff --git a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs index f9f326129..810a236ee 100644 --- a/test/FsAutoComplete.Tests.Lsp/GoToTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/GoToTests.fs @@ -7,6 +7,10 @@ open Helpers open Ionide.LanguageServerProtocol.Types open FsToolkit.ErrorHandling open FsAutoComplete +open Utils.ServerTests +open Utils.Server +open Utils.Utils +open Utils.TextEdit ///GoTo tests let private gotoTest state = @@ -542,9 +546,76 @@ let private scriptGotoTests state = "should point to the range of the definition of `testFunction`" }) ] +let private untitledGotoTests state = + serverTestList "Untitled GoTo Tests" state defaultConfigDto None (fun server -> [ + testCaseAsync "can go to variable declaration" <| async { + let (usagePos, declRange, text) = + """ + let $0x$0 = 1 + let _ = () + let a = $0x + """ + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + |> fun (decl, text) -> + let (pos, text) = + text + |> Cursor.assertExtractPosition + (pos, decl, text) + let! (doc, diags) = server |> Server.createUntitledDocument text + use doc = doc + + let p : TextDocumentPositionParams = { + TextDocument = doc.TextDocumentIdentifier + Position = usagePos + } + let! res = doc.Server.Server.TextDocumentDefinition p + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request none" + | Ok (Some (GotoResult.Multiple _)) -> failtest "Should only get one location" + | Ok (Some (GotoResult.Single r)) -> + Expect.stringEnds r.Uri doc.Uri "should navigate to source file" + Expect.equal r.Range declRange "should point to the range of variable declaration" + } + testCaseAsync "can go to function declaration" <| async { + let (usagePos, declRange, text) = + """ + let $0myFun$0 a b = a + b + let _ = () + let a = my$0Fun 1 1 + """ + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + |> fun (decl, text) -> + let (pos, text) = + text + |> Cursor.assertExtractPosition + (pos, decl, text) + let! (doc, diags) = server |> Server.createUntitledDocument text + use doc = doc + + let p : TextDocumentPositionParams = { + TextDocument = doc.TextDocumentIdentifier + Position = usagePos + } + let! res = doc.Server.Server.TextDocumentDefinition p + match res with + | Error e -> failtestf "Request failed: %A" e + | Ok None -> failtest "Request none" + | Ok (Some (GotoResult.Multiple _)) -> failtest "Should only get one location" + | Ok (Some (GotoResult.Single r)) -> + Expect.stringEnds r.Uri doc.Uri "should navigate to source file" + Expect.equal r.Range declRange "should point to the range of function declaration" + } + ]) + let tests state = testSequenced <| testList - "Go to definition tests" - [ gotoTest state - scriptGotoTests state ] + "Go to definition tests" + [ + gotoTest state + scriptGotoTests state + untitledGotoTests state + ]