diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index e48870a30..5ed9a358b 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -7,7 +7,6 @@ open Fantomas.Client.LSPFantomasService open FsAutoComplete.Logging open FsAutoComplete.UnionPatternMatchCaseGenerator open FsAutoComplete.RecordStubGenerator -open FsAutoComplete.InterfaceStubGenerator open System.Threading open Utils open FSharp.Compiler.CodeAnalysis @@ -1331,23 +1330,6 @@ type Commands |> x.AsCancellable tyRes.FileName |> AsyncResult.recoverCancellation - member x.GetInterfaceStub (tyRes: ParseAndCheckResults) (pos: Position) (lines: NamedText) (lineStr: LineStr) = - async { - let doc = docForText lines tyRes - let! res = tryFindInterfaceExprInBufferAtPos codeGenServer pos doc - - match res with - | None -> return CoreResponse.InfoRes "Interface at position not found" - | Some interfaceData -> - let! stubInfo = handleImplementInterface codeGenServer tyRes pos doc lines lineStr interfaceData - - match stubInfo with - | Some (insertPosition, generatedCode) -> return CoreResponse.Res(generatedCode, insertPosition) - | None -> return CoreResponse.InfoRes "Interface at position not found" - } - |> x.AsCancellable tyRes.FileName - |> AsyncResult.recoverCancellation - member x.GetAbstractClassStub (tyRes: ParseAndCheckResults) (objExprRange: Range) diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index aa46eab79..45b9402eb 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -36,7 +36,6 @@ - diff --git a/src/FsAutoComplete.Core/InterfaceStubGenerator.fs b/src/FsAutoComplete.Core/InterfaceStubGenerator.fs deleted file mode 100644 index 1f251d8d5..000000000 --- a/src/FsAutoComplete.Core/InterfaceStubGenerator.fs +++ /dev/null @@ -1,212 +0,0 @@ -/// Original code from VisualFSharpPowerTools project: https://github.com/fsprojects/VisualFSharpPowerTools/blob/master/src/FSharp.Editing/CodeGeneration/InterfaceStubGenerator.fs -module FsAutoComplete.InterfaceStubGenerator - -open System -open FSharp.Compiler.Syntax -open FSharp.Compiler.Text -open FSharp.Compiler.Symbols -open FsAutoComplete.CodeGenerationUtils -open FSharp.Compiler.Tokenization - -/// Capture information about an interface in ASTs -[] -type InterfaceData = - | Interface of SynType * SynMemberDefns option - | ObjExpr of SynType * SynBinding list - member x.Range = - match x with - | InterfaceData.Interface(typ, _) -> - typ.Range - | InterfaceData.ObjExpr(typ, _) -> - typ.Range - member x.TypeParameters = - match x with - | InterfaceData.Interface(typ, _) - | InterfaceData.ObjExpr(typ, _) -> expandTypeParameters typ - -/// Get associated member names and ranges -/// In case of properties, intrinsic ranges might not be correct for the purpose of getting -/// positions of 'member', which indicate the indentation for generating new members -let getMemberNameAndRanges = function - | InterfaceData.Interface(_, None) -> - [] - | InterfaceData.Interface(_, Some memberDefns) -> - memberDefns - |> Seq.choose (function (SynMemberDefn.Member(binding, _)) -> Some binding | _ -> None) - |> Seq.choose (|MemberNameAndRange|_|) - |> Seq.toList - | InterfaceData.ObjExpr(_, bindings) -> - List.choose (|MemberNameAndRange|_|) bindings - -let private walkTypeDefn pos (SynTypeDefn(members = members; implicitConstructor = implicitCtor)) = - Option.toList implicitCtor @ members - |> List.tryPick (fun m -> - if Range.rangeContainsPos m.Range pos - then - match m with - | SynMemberDefn.Interface(interfaceType = iface; members = members) -> - Some (InterfaceData.Interface(iface, members)) - | _ -> None - else - None - ) - -let tryFindInterfaceDeclAt (pos: Position) (tree: ParsedInput) = - SyntaxTraversal.Traverse(pos, tree, { - new SyntaxVisitorBase<_>() with - member _.VisitExpr (_, _, defaultTraverse, expr) = - match expr with - SynExpr.ObjExpr(objType = ty; argOptions = baseCallOpt; bindings = binds; extraImpls = ifaces) -> - match baseCallOpt with - | None -> - if Range.rangeContainsPos ty.Range pos then - Some (InterfaceData.ObjExpr(ty, binds)) - else - ifaces - |> List.tryPick (fun (SynInterfaceImpl(interfaceTy = ty; bindings = binds; range = range)) -> - if Range.rangeContainsPos range pos then - Some (InterfaceData.ObjExpr(ty, binds)) - else None - ) - | Some _ -> None - | _ -> defaultTraverse expr - override _.VisitModuleDecl (_, defaultTraverse, decl) = - match decl with - | SynModuleDecl.Types(types, _) -> - List.tryPick (walkTypeDefn pos) types - | _ -> defaultTraverse decl - }) - -let tryFindInterfaceExprInBufferAtPos (codeGenService: CodeGenerationService) (pos: Position) (document : Document) = - asyncMaybe { - let! parseResults = codeGenService.ParseFileInProject(document.FullName) - return! tryFindInterfaceDeclAt pos parseResults.ParseTree - } - -/// Return the interface identifier -/// Useful, to determine the insert position when no `with` keyword has been found. -let getInterfaceIdentifier (interfaceData : InterfaceData) (tokens : FSharpTokenInfo list) = - let newKeywordIndex = - match interfaceData with - | InterfaceData.ObjExpr _ -> - tokens - // Find the `new` keyword - |> List.findIndex(fun token -> - token.CharClass = FSharpTokenCharKind.Keyword - && token.TokenName = "NEW" - ) - | InterfaceData.Interface _ -> - tokens - // Find the `new` keyword - |> List.findIndex(fun token -> - token.CharClass = FSharpTokenCharKind.Keyword - && token.TokenName = "INTERFACE" - ) - - CodeGenerationUtils.findLastIdentifier tokens.[newKeywordIndex + 2..] tokens.[newKeywordIndex + 2] - -/// Try to find the start column, so we know what the base indentation should be -let inferStartColumn (codeGenServer : CodeGenerationService) (pos : Position) (doc : Document) (lines: ISourceText) (lineStr : string) (interfaceData : InterfaceData) (indentSize : int) = - match getMemberNameAndRanges interfaceData with - | (_, range) :: _ -> - getLineIdent (lines.GetLineString(range.StartLine - 1)) - | [] -> - match interfaceData with - | InterfaceData.Interface _ as iface -> - // 'interface ISomething with' is often in a new line, we use the indentation of that line - getLineIdent lineStr + indentSize - | InterfaceData.ObjExpr _ as iface -> - match codeGenServer.TokenizeLine(doc.FullName, pos.Line) with - | Some tokens -> - tokens - |> List.tryPick (fun (t: FSharpTokenInfo) -> - if t.CharClass = FSharpTokenCharKind.Keyword && t.TokenName = "NEW" then - // We round to nearest so the generated code will align on the indentation guides - findGreaterMultiple (t.LeftColumn + indentSize) indentSize - |> Some - else None) - // There is no reference point, we indent the content at the start column of the interface - |> Option.defaultValue iface.Range.StartColumn - | None -> iface.Range.StartColumn - -/// Return None, if we failed to handle the interface implementation -/// Return Some (insertPosition, generatedString): -/// `insertPosition`: representation the position where the editor should insert the `generatedString` -let handleImplementInterface (codeGenServer : CodeGenerationService) (checkResultForFile: ParseAndCheckResults) (pos : Position) (doc : Document) (lines: ISourceText) (lineStr : string) (interfaceData : InterfaceData) = - async { - let! result = asyncMaybe { - let! _symbol, symbolUse = codeGenServer.GetSymbolAndUseAtPositionOfKind(doc.FullName, pos, SymbolKind.Ident) - let thing = - match symbolUse with - | None -> None - | Some symbolUse -> - match symbolUse.Symbol with - | :? FSharpEntity as entity -> - if isInterface entity then - match checkResultForFile.GetCheckResults.GetDisplayContextForPos(pos) with - | Some displayContext -> - Some (interfaceData, displayContext, entity) - | None -> None - else - None - | _ -> None - return! thing - } - - match result with - | Some (interfaceData, displayContext, entity) -> - let getMemberByLocation (name, range: Range) = - asyncMaybe { - let pos = Position.fromZ (range.StartLine - 1) (range.StartColumn + 1) - return! checkResultForFile.GetCheckResults.GetSymbolUseAtLocation (pos.Line, pos.Column, lineStr, []) - } - - let insertInfo = - match codeGenServer.TokenizeLine(doc.FullName, pos.Line) with - | Some tokens -> findLastPositionOfWithKeyword tokens entity pos (getInterfaceIdentifier interfaceData) - | None -> None - - let desiredMemberNamesAndRanges = getMemberNameAndRanges interfaceData - let! implementedSignatures = - getImplementedMemberSignatures getMemberByLocation displayContext desiredMemberNamesAndRanges - - let generatedString = - let formattedString = - formatMembersAt - (inferStartColumn codeGenServer pos doc lines lineStr interfaceData 4) // 4 here correspond to the indent size - 4 // Should we make it a setting from the IDE ? - interfaceData.TypeParameters - "$objectIdent" - "$methodBody" - displayContext - implementedSignatures - entity - getInterfaceMembers - true // Always generate the verbose version of the code - - // If we are in a object expression, we remove the last new line, so the `}` stay on the same line - match interfaceData with - | InterfaceData.Interface _ -> - formattedString - | InterfaceData.ObjExpr _ -> - formattedString.TrimEnd('\n') - - // If generatedString is empty it means nothing is missing to the interface - // So we return None, in order to not show a "Falsy Hint" - if String.IsNullOrEmpty generatedString then - return None - else - match insertInfo with - | Some (shouldAppendWith, insertPosition) -> - if shouldAppendWith then - return Some (insertPosition, " with" + generatedString) - else - return Some (insertPosition, generatedString) - | None -> - // Unable to find an optimal insert position so return the position under the cursor - // By doing that we allow the user to copy/paste the code if the insertion break the code - // If we return None, then user would not benefit from interface stub generation at all - return Some (pos, generatedString) - | None -> - return None - } diff --git a/src/FsAutoComplete.Core/Utils.fs b/src/FsAutoComplete.Core/Utils.fs index 90f7ff439..b60e6ab72 100644 --- a/src/FsAutoComplete.Core/Utils.fs +++ b/src/FsAutoComplete.Core/Utils.fs @@ -761,3 +761,7 @@ type Debounce<'a>(timeout, fn) = /// Calls the function, after debouncing has been applied. member __.Bounce(arg) = mailbox.Post(arg) + +module Indentation = + let inline get (line: string) = + line.Length - line.AsSpan().Trim(' ').Length diff --git a/src/FsAutoComplete/CodeFixes/GenerateInterfaceStub.fs b/src/FsAutoComplete/CodeFixes/GenerateInterfaceStub.fs deleted file mode 100644 index ec13c5b5a..000000000 --- a/src/FsAutoComplete/CodeFixes/GenerateInterfaceStub.fs +++ /dev/null @@ -1,43 +0,0 @@ -module FsAutoComplete.CodeFix.GenerateInterfaceStub - -open FsToolkit.ErrorHandling -open FsAutoComplete.CodeFix -open FsAutoComplete.CodeFix.Types -open Ionide.LanguageServerProtocol.Types -open FsAutoComplete -open FsAutoComplete.LspHelpers -open System.IO - -/// a codefix that generates member stubs for an interface declaration -let fix (getParseResultsForFile: GetParseResultsForFile) - (genInterfaceStub: _ -> _ -> _ -> _ -> Async>) - (getTextReplacements: unit -> Map) - : CodeFix = - fun codeActionParams -> - asyncResult { - let fileName = - codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath - - let pos = - protocolPosToPos codeActionParams.Range.Start - - let! (tyRes, line, lines) = getParseResultsForFile fileName pos - - match! genInterfaceStub tyRes pos lines line with - | CoreResponse.Res (text, position) -> - let replacements = getTextReplacements () - - let replaced = - (text, replacements) - ||> Seq.fold (fun text (KeyValue (key, replacement)) -> text.Replace(key, replacement)) - - return - [ { SourceDiagnostic = None - Title = "Generate interface stub" - File = codeActionParams.TextDocument - Edits = - [| { Range = fcsPosToProtocolRange position - NewText = replaced } |] - Kind = FixKind.Fix } ] - | _ -> return [] - } diff --git a/src/FsAutoComplete/CodeFixes/ImplementInterface.fs b/src/FsAutoComplete/CodeFixes/ImplementInterface.fs new file mode 100644 index 000000000..703db58b5 --- /dev/null +++ b/src/FsAutoComplete/CodeFixes/ImplementInterface.fs @@ -0,0 +1,383 @@ +module FsAutoComplete.CodeFix.ImplementInterface + +open FsToolkit.ErrorHandling +open FsAutoComplete.CodeFix +open FsAutoComplete.CodeFix.Types +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete +open FsAutoComplete.LspHelpers +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Symbols +open FSharp.Compiler.Syntax +open FSharp.Compiler.Text + +/// `pos` is expected to be on the leading `{` (main interface) or `interface` (additional interfaces) +/// -> `diagnostic.Range.Start` +let private tryFindInterfaceDeclarationInObjectExpression + (pos: Position) + (ast: ParsedInput) + = + SyntaxTraversal.Traverse(pos, ast, { + new SyntaxVisitorBase<_>() with + member _.VisitExpr (_, _, defaultTraverse, expr) = + match expr with + | SynExpr.ObjExpr(objType = ty; bindings=binds; extraImpls=ifaces) -> + ifaces + |> List.tryPick (fun (SynInterfaceImpl(interfaceTy=ty; bindings=binds; range=range)) -> + if Range.rangeContainsPos range pos then + Some (InterfaceData.ObjExpr(ty, binds)) + else + None + ) + |> Option.orElseWith (fun _ -> Some (InterfaceData.ObjExpr(ty, binds))) + + | _ -> defaultTraverse expr + }) + +/// `pos`: on corresponding interface identifier +/// +/// Returns: `(start, with)` +/// `start`: +/// * start of `interface` keyword (type, secondary interface in obj expr) +/// * start of `new` keyword (primary interface in obj expr) +/// -> alignment for members +/// `with`: +/// * range of `with` keyword if exists +/// -> pos for append +let private tryFindInterfaceStartAndWith + (pos: Position) + (ast: ParsedInput) + = + SyntaxTraversal.Traverse(pos, ast, { + new SyntaxVisitorBase<_>() with + member _.VisitExpr (_, _, defaultTraverse, expr) = + match expr with + // main interface + | SynExpr.ObjExpr(objType=ty; withKeyword=withRange; newExprRange=startingAtNewRange) when Range.rangeContainsPos ty.Range pos -> + // { new IDisposable with } + // ^ + let start = startingAtNewRange.Start + Some (start, withRange) + // secondary interface + | SynExpr.ObjExpr(extraImpls=ifaces) -> + ifaces + |> List.tryPick (fun (SynInterfaceImpl(interfaceTy=ty; withKeyword=withRange; range=startingAtInterfaceRange)) -> + if Range.rangeContainsPos ty.Range pos then + // { new IDisposable with + // member this.Dispose() = () + // interface ICloneable with + // ^ + // } + let start = startingAtInterfaceRange.Start + Some (start, withRange) + else + None + ) + |> Option.orElseWith (fun _ -> defaultTraverse expr) + | _ -> defaultTraverse expr + member _.VisitModuleDecl (_, defaultTraverse, synModuleDecl) = + match synModuleDecl with + | SynModuleDecl.Types(typeDefns, _) -> + let typeDefn = + typeDefns + |> List.tryFind (fun typeDef -> Range.rangeContainsPos typeDef.Range pos) + match typeDefn with + | Some (SynTypeDefn(typeRepr=typeRepr; members=members)) -> + let tryFindInMemberDefns + (members: SynMemberDefns) + = + members + |> List.tryPick ( + function + | SynMemberDefn.Interface (interfaceType=ty; withKeyword=withRange; range=range) when Range.rangeContainsPos ty.Range pos -> + // interface IDisposable with + // ^ + let start = range.Start + Some (start, withRange) + | _ -> None + ) + + match typeRepr with + | SynTypeDefnRepr.ObjectModel (members=members) -> + // in class (-> in typeRepr) + tryFindInMemberDefns members + | _ -> None + |> Option.orElseWith (fun _ -> + // in union, records (-> in members) + tryFindInMemberDefns members + ) + |> Option.orElseWith (fun _ -> defaultTraverse synModuleDecl) + | _ -> defaultTraverse synModuleDecl + | _ -> defaultTraverse synModuleDecl + }) + +type private InsertionData = { + /// Indentation of new members + StartColumn: int + /// Insert position of new members + InsertAt: Position + /// `true`: no existing `with` + /// -> Insert before members at `InsertAt` + InsertWith: bool + // Handled elsewhere: + // Detected via diagnostics.Range.End & lookup in source + // InsertClosingBracket: bool +} +let private tryFindInsertionData + (interfaceData: InterfaceData) + (ast: ParsedInput) + (indentationSize: int) + = + + let lastExistingMember = + match interfaceData with + | InterfaceData.Interface(_, None) -> None + | InterfaceData.Interface(_, Some memberDefns) -> + memberDefns + |> List.choose (function | SynMemberDefn.Member(memberDefn=binding) -> Some binding | _ -> None) + |> List.tryLast + | InterfaceData.ObjExpr(_, bindings) -> + bindings + |> List.tryLast + + match lastExistingMember with + | Some (SynBinding(attributes=attributes; valData=SynValData(memberFlags=memberFlags); headPat=headPat; expr=expr)) -> + // align with existing member + // insert after last member + + // alignment: + // ```fsharp + // type A () = + // interface IDisposable with + // /// hello world + // [] + // member + // _.Dispose () = () + // ``` + // -> use first non-comment (-> most left) + // + // Note: illegal: + // ```fsharp + // interface IDisposable with + // (*foo bar*)[] + // member + // _.Dispose () = () + // + // interface IDisposable with + // (*foo bar*)member + // _.Dispose () = () + // ``` + // -> first non-comment is indentation required by F# + // But valid: + // ```fsharp + // interface IDisposable with + // (*foo bar*)member + // (*baaaaz*)_.Dispose () = () + // + // interface IDisposable with + // (*foo bar*)member + // (*baaaaaaaaaz*)_.Dispose () = () + // ``` + // (`_` (this) behind `member`) + + let startCol = + // attribute must be first + attributes + |> List.tryHead + |> Option.map (fun attr -> attr.Range.StartColumn) + |> Option.orElseWith (fun _ -> + // leftmost `member` or `override` (and just to be sure: `default`, `abstract` or `static`) + match memberFlags with + | Some memberFlags -> + let trivia = memberFlags.Trivia + [ + trivia.StaticRange + trivia.MemberRange + trivia.OverrideRange + trivia.AbstractRange + trivia.DefaultRange + ] + |> List.choose id + |> List.map (fun r -> r.StartColumn) + // List.tryMin + |> List.fold (fun m c -> + match m with + | None -> Some c + | Some m -> + min c m + |> Some + ) None + | None -> None + ) + |> Option.defaultValue + // fallback: start of head pat (should not happen -> always `member`) + headPat.Range.StartColumn + + let insertPos = expr.Range.End + + { + StartColumn = startCol + InsertAt = insertPos + InsertWith = false + } + |> Some + | None -> + // align with `interface` or `new` + // no attributes on interface allowed + // insert after `with` or identifier + match tryFindInterfaceStartAndWith interfaceData.Range.End ast with + | None -> None + | Some (startPos, withRange) -> + let startCol = startPos.Column + indentationSize + let insertPos = + withRange + |> Option.map (fun r -> r.End) + |> Option.defaultValue interfaceData.Range.End + + { + StartColumn = startCol + InsertAt = insertPos + InsertWith = withRange |> Option.isNone + } + |> Some + +type Config = { + ObjectIdentifier: string + MethodBody: string + IndentationSize: int +} + +let titleWithTypeAnnotation = "Implement interface" +let titleWithoutTypeAnnotation = "Implement interface without type annotation" + +/// codefix that generates members for an interface implementation +let fix + (getParseResultsForFile: GetParseResultsForFile) + (getProjectOptionsForFile: GetProjectOptionsForFile) + (config: unit -> Config) + : CodeFix = + Run.ifDiagnosticByCode + (Set.ofList ["366"]) + (fun diagnostic codeActionParams -> asyncResult { + // diagnostic range: + // * object expression: + // * main interface: full expression from starting `{` to ending `}` + // * sub interface: `interface` to ending `}` + // * implement interface in type: only interface name + + let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath + let startPos = protocolPosToPos codeActionParams.Range.Start + let! (tyRes, line, lines) = getParseResultsForFile fileName startPos + + let! interfaceData = + InterfaceStubGenerator.TryFindInterfaceDeclaration startPos tyRes.GetAST + |> Option.orElseWith (fun _ -> + // happens when in object expression (`startPos` is on `{` or `interface`, NOT interface name) + tryFindInterfaceDeclarationInObjectExpression startPos tyRes.GetAST + ) + |> Result.ofOption (fun _ -> "No interface at position") + + /// End of Interface identifier + let ifacePos = + match interfaceData with + | InterfaceData.ObjExpr (ty, _) -> ty.Range.End + | InterfaceData.Interface(ty, _) -> ty.Range.End + // line might be different -> update + // (for example when `{` not on same line as main interface name) + let! line = + if ifacePos.Line <> startPos.Line then lines.GetLine(ifacePos) else Some line + |> Result.ofOption (fun _ -> "Invalid position") + + let! symbolUse = + tyRes.TryGetSymbolUse ifacePos line + |> Result.ofOption (fun _ -> "No symbol use at position") + + match symbolUse.Symbol with + | :? FSharpEntity as entity when + InterfaceStubGenerator.IsInterface entity + && + not (InterfaceStubGenerator.HasNoInterfaceMember entity) + -> + + let existingMembers = InterfaceStubGenerator.GetMemberNameAndRanges interfaceData + let interfaceMembers = InterfaceStubGenerator.GetInterfaceMembers entity + if List.length existingMembers <> Seq.length interfaceMembers then + let getMemberByLocation(name, range: FcsRange) = + match lines.GetLine range.End with + | None -> None + | Some lineStr -> tyRes.GetCheckResults.GetSymbolUseAtLocation(range.EndLine, range.EndColumn, lineStr, [name]) + let! implementedMemberSignatures = + InterfaceStubGenerator.GetImplementedMemberSignatures + getMemberByLocation + symbolUse.DisplayContext + interfaceData + + let config = config () + + let! insertionData = + tryFindInsertionData interfaceData tyRes.GetAST config.IndentationSize + |> Result.ofOption (fun _ -> "No insert location found") + + let appendWithEdit = + if insertionData.InsertWith then + { + Range = fcsPosToProtocolRange insertionData.InsertAt + NewText = " with" + } + |> Some + else + None + let appendClosingBracketEdit = + match interfaceData with + | InterfaceData.ObjExpr _ -> + // `diagnostic.Range`: + // * main interface: over full range of object expression (opening `{` to closing `}`) + // * sub interface: `interface XXX with` to closing `}` + match lines.TryGetChar (protocolPosToPos diagnostic.Range.End) with + | Some '}' -> None + | _ -> + let pos = diagnostic.Range.End + { + Range = { Start = pos; End = pos } + NewText = " }" + } + |> Some + | _ -> None + + let getMainEdit withTypeAnnotation = + let stub = + let stub = + InterfaceStubGenerator.FormatInterface + insertionData.StartColumn config.IndentationSize interfaceData.TypeParameters config.ObjectIdentifier config.MethodBody + symbolUse.DisplayContext implementedMemberSignatures entity withTypeAnnotation + stub.TrimEnd(System.Environment.NewLine.ToCharArray()) + { + Range = fcsPosToProtocolRange insertionData.InsertAt + NewText = stub + } + + let getFix title (mainEdit: TextEdit) = + { + Title = title + File = codeActionParams.TextDocument + SourceDiagnostic = Some diagnostic + Kind = FixKind.Fix + Edits = [| + match appendWithEdit with + | Some edit -> edit | None -> () + + mainEdit + + match appendClosingBracketEdit with + | Some edit -> edit | None -> () + |] + } + + return [ + getFix titleWithTypeAnnotation (getMainEdit true) + getFix titleWithoutTypeAnnotation (getMainEdit false) + ] + else + return [] + | _ -> return [] + }) diff --git a/src/FsAutoComplete/FsAutoComplete.Lsp.fs b/src/FsAutoComplete/FsAutoComplete.Lsp.fs index dc8ff6075..3c34b6a0d 100644 --- a/src/FsAutoComplete/FsAutoComplete.Lsp.fs +++ b/src/FsAutoComplete/FsAutoComplete.Lsp.fs @@ -820,11 +820,12 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS commands.TryGetFileCheckerOptionsWithLines >> Result.map fst - let interfaceStubReplacements () = - Map.ofList [ "$objectIdent", config.InterfaceStubGenerationObjectIdentifier - "$methodBody", config.InterfaceStubGenerationMethodBody ] - - let getInterfaceStubReplacements () = interfaceStubReplacements () + let implementInterfaceConfig () : ImplementInterface.Config = + { + ObjectIdentifier = config.InterfaceStubGenerationObjectIdentifier + MethodBody = config.InterfaceStubGenerationMethodBody + IndentationSize = config.IndentationSize + } let unionCaseStubReplacements () = Map.ofList [ "$1", config.UnionCaseStubGenerationBody ] @@ -865,7 +866,11 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS ExternalSystemDiagnostics.analyzers Run.ifEnabled (fun _ -> config.InterfaceStubGeneration) - (GenerateInterfaceStub.fix tryGetParseResultsForFile commands.GetInterfaceStub getInterfaceStubReplacements) + (ImplementInterface.fix + tryGetParseResultsForFile + tryGetProjectOptions + implementInterfaceConfig + ) Run.ifEnabled (fun _ -> config.RecordStubGeneration) (GenerateRecordStub.fix tryGetParseResultsForFile commands.GetRecordStub getRecordStubReplacements) @@ -898,7 +903,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS AddMissingInstanceMember.fix AddExplicitTypeToParameter.fix tryGetParseResultsForFile ConvertPositionalDUToNamed.fix tryGetParseResultsForFile getRangeText - UseTripleQuotedInterpolation.fix tryGetParseResultsForFile getRangeText + UseTripleQuotedInterpolation.fix tryGetParseResultsForFile getRangeText RenameParamToMatchSignature.fix tryGetParseResultsForFile |] diff --git a/src/FsAutoComplete/LspHelpers.fs b/src/FsAutoComplete/LspHelpers.fs index 7b3188a4d..8baeffa6c 100644 --- a/src/FsAutoComplete/LspHelpers.fs +++ b/src/FsAutoComplete/LspHelpers.fs @@ -580,6 +580,7 @@ type FSharpConfigDto = { ExternalAutocomplete: bool option Linter: bool option LinterConfig: string option + IndentationSize: int option UnionCaseStubGeneration: bool option UnionCaseStubGenerationBody: string option RecordStubGeneration: bool option @@ -620,6 +621,7 @@ type FSharpConfig = { ExternalAutocomplete: bool Linter: bool LinterConfig: string option + IndentationSize: int UnionCaseStubGeneration: bool UnionCaseStubGenerationBody: string RecordStubGeneration: bool @@ -656,6 +658,7 @@ with ExternalAutocomplete = false Linter = false LinterConfig = None + IndentationSize = 4 UnionCaseStubGeneration = false UnionCaseStubGenerationBody = """failwith "Not Implemented" """ RecordStubGeneration = false @@ -695,6 +698,7 @@ with ExternalAutocomplete = defaultArg dto.ExternalAutocomplete false Linter = defaultArg dto.Linter false LinterConfig = dto.LinterConfig + IndentationSize = defaultArg dto.IndentationSize 4 UnionCaseStubGeneration = defaultArg dto.UnionCaseStubGeneration false UnionCaseStubGenerationBody = defaultArg dto.UnionCaseStubGenerationBody "failwith \"Not Implemented\"" RecordStubGeneration = defaultArg dto.RecordStubGeneration false @@ -742,6 +746,7 @@ with ExternalAutocomplete = defaultArg dto.ExternalAutocomplete x.ExternalAutocomplete Linter = defaultArg dto.Linter x.Linter LinterConfig = dto.LinterConfig + IndentationSize = defaultArg dto.IndentationSize x.IndentationSize UnionCaseStubGeneration = defaultArg dto.UnionCaseStubGeneration x.UnionCaseStubGeneration UnionCaseStubGenerationBody = defaultArg dto.UnionCaseStubGenerationBody x.UnionCaseStubGenerationBody RecordStubGeneration = defaultArg dto.RecordStubGeneration x.RecordStubGeneration diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddExplicitTypeToParameterTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddExplicitTypeToParameterTests.fs new file mode 100644 index 000000000..38c2255fb --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/AddExplicitTypeToParameterTests.fs @@ -0,0 +1,367 @@ +module private FsAutoComplete.Tests.CodeFixTests.AddExplicitTypeToParameterTests + +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix + +let tests state = + serverTestList (nameof AddExplicitTypeToParameter) state defaultConfigDto None (fun server -> [ + let selectCodeFix = CodeFix.withTitle AddExplicitTypeToParameter.title + testCaseAsync "can suggest explicit parameter for record-typed function parameters" <| + CodeFix.check server + """ + type Foo = + { name: string } + + let name $0f = + f.name + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + type Foo = + { name: string } + + let name (f: Foo) = + f.name + """ + testCaseAsync "can add type for int param" <| + CodeFix.check server + """ + let f ($0x) = x + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x: int) = x + 1 + """ + testCaseAsync "can add type for generic param" <| + CodeFix.check server + """ + let f ($0x) = () + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x: 'a) = () + """ + testCaseAsync "doesn't trigger when existing type" <| + CodeFix.checkNotApplicable server + """ + let f ($0x: int) = () + """ + (Diagnostics.acceptAll) + selectCodeFix + testCaseAsync "can add type to tuple item" <| + CodeFix.check server + """ + let f (a, $0b, c) = a + b + c + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (a, b: int, c) = a + b + c + 1 + """ + testCaseAsync "doesn't trigger in tuple when existing type" <| + CodeFix.checkNotApplicable server + """ + let f (a, $0b: int, c) = a + b + c + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + testCaseAsync "can add type to 2nd of 3 param" <| + CodeFix.check server + """ + let f a $0b c = a + b + c + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f a (b: int) c = a + b + c + 1 + """ + testCaseAsync "doesn't trigger on 2nd of 3 param when existing type" <| + CodeFix.checkNotApplicable server + """ + let f a ($0b: int) c = a + b + c + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + testCaseAsync "can add type to 2nd of 3 param when other params have types" <| + CodeFix.check server + """ + let f (a: int) $0b (c: int) = a + b + c + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (a: int) (b: int) (c: int) = a + b + c + 1 + """ + testCaseAsync "can add type to member param" <| + CodeFix.check server + """ + type A() = + member _.F($0a) = a + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + type A() = + member _.F(a: int) = a + 1 + """ + testCaseAsync "doesn't trigger for member param when existing type" <| + CodeFix.checkNotApplicable server + """ + type A() = + member _.F($0a: int) = a + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + testCaseAsync "can add type to ctor param" <| + CodeFix.check server + """ + type A($0a) = + member _.F() = a + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + type A(a: int) = + member _.F() = a + 1 + """ + testCaseAsync "doesn't trigger for ctor param when existing type" <| + CodeFix.checkNotApplicable server + """ + type A($0a: int) = + member _.F() = a + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + testCaseAsync "can add type to correct ctor param" <| + CodeFix.check server + """ + type A(str, $0n, b) = + member _.FString() = sprintf "str=%s" str + member _.FInt() = n + 1 + member _.FBool() = sprintf "b=%b" b + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + type A(str, n: int, b) = + member _.FString() = sprintf "str=%s" str + member _.FInt() = n + 1 + member _.FBool() = sprintf "b=%b" b + """ + testCaseAsync "doesn't trigger for ctor param when existing type and multiple params" <| + CodeFix.checkNotApplicable server + """ + type A(str, $0n: int, b) = + member _.FString() = sprintf "str=%s" str + member _.FInt() = a + 1 + member _.FBool() = sprintf "b=%b" b + """ + (Diagnostics.acceptAll) + selectCodeFix + testCaseAsync "can add type to secondary ctor param" <| + CodeFix.check server + """ + type A(a) = + new($0a, b) = A(a+b) + member _.F() = a + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + type A(a) = + new(a: int, b) = A(a+b) + member _.F() = a + 1 + """ + testList "parens" [ + testCaseAsync "single param without parens -> add parens" <| + CodeFix.check server + """ + let f $0x = x + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x: int) = x + 1 + """ + testCaseAsync "single param with parens -> keep parens" <| + CodeFix.check server + """ + let f ($0x) = x + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x: int) = x + 1 + """ + testCaseAsync "multi params without parens -> add parens" <| + CodeFix.check server + """ + let f a $0x y = x + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f a (x: int) y = x + 1 + """ + testCaseAsync "multi params with parens -> keep parens" <| + CodeFix.check server + """ + let f a ($0x) y = x + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f a (x: int) y = x + 1 + """ + testList "tuple params without parens -> no parens" [ + testCaseAsync "start" <| + CodeFix.check server + """ + let f ($0x, y, z) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x: int, y, z) = x + y + z + 1 + """ + testCaseAsync "center" <| + CodeFix.check server + """ + let f (x, $0y, z) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x, y: int, z) = x + y + z + 1 + """ + testCaseAsync "end" <| + CodeFix.check server + """ + let f (x, y, $0z) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x, y, z: int) = x + y + z + 1 + """ + ] + testList "tuple params with parens -> keep parens" [ + testCaseAsync "start" <| + CodeFix.check server + """ + let f (($0x), y, z) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f ((x: int), y, z) = x + y + z + 1 + """ + testCaseAsync "center" <| + CodeFix.check server + """ + let f (x, ($0y), z) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x, (y: int), z) = x + y + z + 1 + """ + testCaseAsync "end" <| + CodeFix.check server + """ + let f (x, y, ($0z)) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f (x, y, (z: int)) = x + y + z + 1 + """ + ] + testList "tuple params without parens but spaces -> no parens" [ + testCaseAsync "start" <| + CodeFix.check server + """ + let f ( $0x , y , z ) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f ( x: int , y , z ) = x + y + z + 1 + """ + testCaseAsync "center" <| + CodeFix.check server + """ + let f ( x , $0y , z ) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f ( x , y: int , z ) = x + y + z + 1 + """ + testCaseAsync "end" <| + CodeFix.check server + """ + let f ( x , y , $0z ) = x + y + z + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f ( x , y , z: int ) = x + y + z + 1 + """ + ] + testList "long tuple params without parens but spaces -> no parens" [ + testCaseAsync "start" <| + CodeFix.check server + """ + let f ( xV$0alue , yAnotherValue , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f ( xValue: int , yAnotherValue , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 + """ + testCaseAsync "center" <| + CodeFix.check server + """ + let f ( xValue , yAn$0otherValue , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f ( xValue , yAnotherValue: int , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 + """ + testCaseAsync "end" <| + CodeFix.check server + """ + let f ( xValue , yAnotherValue , zFina$0lValue ) = xValue + yAnotherValue + zFinalValue + 1 + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + let f ( xValue , yAnotherValue , zFinalValue: int ) = xValue + yAnotherValue + zFinalValue + 1 + """ + ] + testCaseAsync "never add parens to primary ctor param" <| + CodeFix.check server + """ + type A ( + $0a + ) = + member _.F(b) = a + b + """ + (Diagnostics.acceptAll) + selectCodeFix + """ + type A ( + a: int + ) = + member _.F(b) = a + b + """ + ] + ]) diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/HelpersTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/HelpersTests.fs new file mode 100644 index 000000000..070e492ff --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/HelpersTests.fs @@ -0,0 +1,164 @@ +module private FsAutoComplete.Tests.CodeFixTests.HelpersTests +// `src\FsAutoComplete\CodeFixes.fs` -> `FsAutoComplete.CodeFix` + +open Expecto +open FsAutoComplete.CodeFix +open Navigation +open FSharp.Compiler.Text +open Utils.TextEdit +open Ionide.LanguageServerProtocol.Types + +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 +] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ImplementInterfaceTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ImplementInterfaceTests.fs new file mode 100644 index 000000000..6a8b69dd6 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/ImplementInterfaceTests.fs @@ -0,0 +1,989 @@ +module private FsAutoComplete.Tests.CodeFixTests.ImplementInterfaceTests + +open Expecto +open Helpers +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix + + +let tests state = + let selectCodeFixWithTypeAnnotation = CodeFix.withTitle ImplementInterface.titleWithTypeAnnotation + let selectCodeFixWithoutTypeAnnotation = CodeFix.withTitle ImplementInterface.titleWithoutTypeAnnotation + let validateDiags = Diagnostics.expectCode "366" + let testBoth server name beforeWithCursor expectedWithTypeAnnotation expectedWithoutTypeAnnotation = + testList name [ + testCaseAsync "with type annotation" <| + CodeFix.check server + beforeWithCursor + validateDiags + selectCodeFixWithTypeAnnotation + expectedWithTypeAnnotation + testCaseAsync "without type annotation" <| + CodeFix.check server + beforeWithCursor + validateDiags + selectCodeFixWithoutTypeAnnotation + expectedWithoutTypeAnnotation + ] + // Note: there's a space after each generated `=` when linebreak! (-> from FCS) + testList (nameof ImplementInterface) [ + let config = { + defaultConfigDto with + IndentationSize = Some 2 + InterfaceStubGeneration = Some true + InterfaceStubGenerationObjectIdentifier = Some "_" + InterfaceStubGenerationMethodBody = Some "failwith \"-\"" + } + + serverTestList "with 2 indentation" state config None (fun server -> [ + let testBoth = testBoth server + testList "in type" [ + testBoth "can implement single interface with single method with existing with" + """ + type X() = + interface System.$0IDisposable with + """ + """ + type X() = + interface System.IDisposable with + member _.Dispose(): unit = + failwith "-" + """ + """ + type X() = + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + testBoth "can implement single interface with single method without existing with" + """ + type X() = + interface System.$0IDisposable + """ + """ + type X() = + interface System.IDisposable with + member _.Dispose(): unit = + failwith "-" + """ + """ + type X() = + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + testBoth "can implement single interface with multiple methods (none already specified)" + """ + type IPrinter = + abstract member Print: format: string -> unit + abstract member Indentation: int with get,set + abstract member Disposed: bool + + type Printer() = + interface $0IPrinter with + """ + """ + type IPrinter = + abstract member Print: format: string -> unit + abstract member Indentation: int with get,set + abstract member Disposed: bool + + type Printer() = + interface IPrinter with + member _.Disposed: bool = + failwith "-" + member _.Indentation + with get (): int = + failwith "-" + and set (v: int): unit = + failwith "-" + member _.Print(format: string): unit = + failwith "-" + """ + """ + type IPrinter = + abstract member Print: format: string -> unit + abstract member Indentation: int with get,set + abstract member Disposed: bool + + type Printer() = + interface IPrinter with + member _.Disposed = failwith "-" + member _.Indentation + with get (): int = + failwith "-" + and set (v: int): unit = + failwith "-" + member _.Print(format) = failwith "-" + """ + testBoth "can implement setter when existing getter" + """ + type IPrinter = + abstract member Indentation: int with get,set + + type Printer() = + interface $0IPrinter with + member _.Indentation with get () = 42 + """ + """ + type IPrinter = + abstract member Indentation: int with get,set + + type Printer() = + interface IPrinter with + member _.Indentation with get () = 42 + member _.Indentation + with set (v: int): unit = + failwith "-" + """ + """ + type IPrinter = + abstract member Indentation: int with get,set + + type Printer() = + interface IPrinter with + member _.Indentation with get () = 42 + member _.Indentation + with set (v: int): unit = + failwith "-" + """ + testBoth "can implement interface member without parameter name" + """ + type I = + abstract member DoStuff: int -> unit + + type T() = + interface $0I with + """ + """ + type I = + abstract member DoStuff: int -> unit + + type T() = + interface I with + member _.DoStuff(arg1: int): unit = + failwith "-" + """ + """ + type I = + abstract member DoStuff: int -> unit + + type T() = + interface I with + member _.DoStuff(arg1) = failwith "-" + """ + testBoth "can implement when one member already implemented" + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + type T() = + interface $0I with + member _.DoOtherStuff value name = name + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + type T() = + interface I with + member _.DoOtherStuff value name = name + member _.DoStuff(value: int): unit = + failwith "-" + member _.Value: int = + failwith "-" + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + type T() = + interface I with + member _.DoOtherStuff value name = name + member _.DoStuff(value) = failwith "-" + member _.Value = failwith "-" + """ + testBoth "can implement when two members already implemented" + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + type T() = + interface $0I with + member _.DoOtherStuff value name = name + member _.Value: int = failwith "-" + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + type T() = + interface I with + member _.DoOtherStuff value name = name + member _.Value: int = failwith "-" + member _.DoStuff(value: int): unit = + failwith "-" + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + type T() = + interface I with + member _.DoOtherStuff value name = name + member _.Value: int = failwith "-" + member _.DoStuff(value) = failwith "-" + """ + testBoth "can implement interface with existing class members" + """ + type T() = + let v = 4 + member _.DoStuff value = v + value + interface System.$0IDisposable with + """ + """ + type T() = + let v = 4 + member _.DoStuff value = v + value + interface System.IDisposable with + member _.Dispose(): unit = + failwith "-" + """ + """ + type T() = + let v = 4 + member _.DoStuff value = v + value + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + testBoth "can implement in record" + """ + type A = + { + Value: int + } + interface System.$0IDisposable with + """ + """ + type A = + { + Value: int + } + interface System.IDisposable with + member _.Dispose(): unit = + failwith "-" + """ + """ + type A = + { + Value: int + } + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + testBoth "can implement in union" + """ + type U = + | A of int + | B of int * string + | C + interface System.$0IDisposable with + """ + """ + type U = + | A of int + | B of int * string + | C + interface System.IDisposable with + member _.Dispose(): unit = + failwith "-" + """ + """ + type U = + | A of int + | B of int * string + | C + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + ] + testList "in object expression" [ + testBoth "can implement single interface with single method with existing with and } in same line" + """ + { new System.$0IDisposable with } + """ + """ + { new System.IDisposable with + member _.Dispose(): unit = + failwith "-" } + """ + """ + { new System.IDisposable with + member _.Dispose() = failwith "-" } + """ + testBoth "can implement single interface with single method without existing with and with } in same line" + """ + { new System.$0IDisposable } + """ + """ + { new System.IDisposable with + member _.Dispose(): unit = + failwith "-" } + """ + """ + { new System.IDisposable with + member _.Dispose() = failwith "-" } + """ + testBoth "can implement single interface with single method without existing with and without } in same line" + """ + { new System.$0IDisposable + """ + """ + { new System.IDisposable with + member _.Dispose(): unit = + failwith "-" } + """ + """ + { new System.IDisposable with + member _.Dispose() = failwith "-" } + """ + // Note: `{ new System.IDisposable with` doesn't raise `FS0366` -> no CodeFix + + testBoth "can implement single interface with multiple methods" + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + { new $0I with } + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + { new I with + member _.DoOtherStuff(value: int) (name: string): string = + failwith "-" + member _.DoStuff(value: int): unit = + failwith "-" + member _.Value: int = + failwith "-" } + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + { new I with + member _.DoOtherStuff value name = failwith "-" + member _.DoStuff(value) = failwith "-" + member _.Value = failwith "-" } + """ + testBoth "can implement interface with multiple methods with one method already implemented" + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + { new $0I with + member this.DoStuff(value: int): unit = + let v = value + 4 + printfn "Res=%i" v + } + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + { new I with + member this.DoStuff(value: int): unit = + let v = value + 4 + printfn "Res=%i" v + member _.DoOtherStuff(value: int) (name: string): string = + failwith "-" + member _.Value: int = + failwith "-" + } + """ + """ + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> name:string -> string + abstract member Value: int + + { new I with + member this.DoStuff(value: int): unit = + let v = value + 4 + printfn "Res=%i" v + member _.DoOtherStuff value name = failwith "-" + member _.Value = failwith "-" + } + """ + testBoth "can trigger with { and } on different lines" + """ + { + new System.$0IDisposable with + } + """ + """ + { + new System.IDisposable with + member _.Dispose(): unit = + failwith "-" + } + """ + """ + { + new System.IDisposable with + member _.Dispose() = failwith "-" + } + """ + testBoth "can implement sub interface" + """ + { new System.IDisposable with + member _.Dispose() = () + interface System.$0ICloneable with + } + """ + """ + { new System.IDisposable with + member _.Dispose() = () + interface System.ICloneable with + member _.Clone(): obj = + failwith "-" + } + """ + """ + { new System.IDisposable with + member _.Dispose() = () + interface System.ICloneable with + member _.Clone() = failwith "-" + } + """ + testBoth "can trigger with cursor on {" + """ + { new System.IDisposable with + member _.Dispose() = () + interface System.ICloneable with + }$0 + """ + """ + { new System.IDisposable with + member _.Dispose() = () + interface System.ICloneable with + member _.Clone(): obj = + failwith "-" + } + """ + """ + { new System.IDisposable with + member _.Dispose() = () + interface System.ICloneable with + member _.Clone() = failwith "-" + } + """ + ] + testList "cursor position" [ + testList "type" [ + // diagnostic range is just interface name + // -> triggers only with cursor on interface name + testCaseAsync "start of name" <| + CodeFix.check server + """ + type T() = + interface $0System.IDisposable with + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + type T() = + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + testCaseAsync "end of name" <| + CodeFix.check server + """ + type T() = + interface System.IDisposable$0 with + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + type T() = + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + testCaseAsync "middle of name" <| + CodeFix.check server + """ + type T() = + interface System.IDisp$0osable with + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + type T() = + interface System.IDisposable with + member _.Dispose() = failwith "-" + """ + ] + testList "object expression" [ + // diagnostic range: + // * main interface: over complete obj expr (start of `{` to end of `}`) + // * sub interface: start of `interface` to end of `}` + testList "main" [ + testList "all on same line" [ + testCaseAsync "{" <| + CodeFix.check server + """ + $0{ new System.IDisposable with } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { new System.IDisposable with + member _.Dispose() = failwith "-" } + """ + testCaseAsync "new" <| + CodeFix.check server + """ + { $0new System.IDisposable with } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { new System.IDisposable with + member _.Dispose() = failwith "-" } + """ + testCaseAsync "with" <| + CodeFix.check server + """ + { new System.IDisposable w$0ith } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { new System.IDisposable with + member _.Dispose() = failwith "-" } + """ + testCaseAsync "}" <| + CodeFix.check server + """ + { new System.IDisposable with $0} + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { new System.IDisposable with + member _.Dispose() = failwith "-" } + """ + ] + testList "on different lines" [ + testCaseAsync "{" <| + CodeFix.check server + """ + $0{ + new System.IDisposable with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { + new System.IDisposable with + member _.Dispose() = failwith "-" + } + """ + testCaseAsync "new" <| + CodeFix.check server + """ + { + n$0ew System.IDisposable with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { + new System.IDisposable with + member _.Dispose() = failwith "-" + } + """ + testCaseAsync "interface name" <| + CodeFix.check server + """ + { + new System.IDis$0posable with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { + new System.IDisposable with + member _.Dispose() = failwith "-" + } + """ + testCaseAsync "with" <| + CodeFix.check server + """ + { + new System.IDisposable wi$0th + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { + new System.IDisposable with + member _.Dispose() = failwith "-" + } + """ + testCaseAsync "}" <| + CodeFix.check server + """ + { + new System.IDisposable with + }$0 + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + { + new System.IDisposable with + member _.Dispose() = failwith "-" + } + """ + ] + ] + testList "sub" [ + testCaseAsync "interface" <| + CodeFix.check server + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + in$0terface ICloneable with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + interface ICloneable with + member _.Clone() = failwith "-" + } + """ + testCaseAsync "interface name" <| + CodeFix.check server + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + interface IClo$0neable with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + interface ICloneable with + member _.Clone() = failwith "-" + } + """ + testCaseAsync "with" <| + CodeFix.check server + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + interface ICloneable wi$0th + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + interface ICloneable with + member _.Clone() = failwith "-" + } + """ + testCaseAsync "}" <| + CodeFix.check server + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + interface ICloneable with + }$0 + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new IDisposable with + member _.Dispose() = failwith "-" + interface ICloneable with + member _.Clone() = failwith "-" + } + """ + ] + ] + ] + testList "strange existing formatting" [ + testList "type" [ + testCaseAsync "interface on prev line" <| + CodeFix.check server + """ + open System + type A () = + interface + $0IDisposable with + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + type A () = + interface + IDisposable with + member _.Dispose() = failwith "-" + """ + testCaseAsync "with on next line" <| + CodeFix.check server + """ + open System + type A () = + interface $0IDisposable + with + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + type A () = + interface IDisposable + with + member _.Dispose() = failwith "-" + """ + testCaseAsync "interface and with on extra lines" <| + CodeFix.check server + """ + open System + type A () = + interface + $0IDisposable + with + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + type A () = + interface + IDisposable + with + member _.Dispose() = failwith "-" + """ + testCaseAsync "attribute" <| + CodeFix.check server + """ + open System + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> string + + type A () = + interface $0I with + [] + member _.DoStuff value = () + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> string + + type A () = + interface I with + [] + member _.DoStuff value = () + member _.DoOtherStuff(value) = failwith "-" + """ + testCaseAsync "inline comment" <| + CodeFix.check server + """ + open System + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> string + + type A () = + interface $0I with + (*foo bar*)[] + (*baaaaaaaaaaaz*)member _.DoStuff value = () + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + // new member must be at least aligned to Attribute + """ + open System + type I = + abstract member DoStuff: value:int -> unit + abstract member DoOtherStuff: value:int -> string + + type A () = + interface I with + (*foo bar*)[] + (*baaaaaaaaaaaz*)member _.DoStuff value = () + member _.DoOtherStuff(value) = failwith "-" + """ + ] + testList "obj expr" [ + testCaseAsync "with on next line" <| + CodeFix.check server + """ + open System + { + new IDis$0posable + with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new IDisposable + with + member _.Dispose() = failwith "-" + } + """ + testCaseAsync "with 3 lines below with comments" <| + CodeFix.check server + """ + open System + { + new IDis$0posable + // some + // comment + with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new IDisposable + // some + // comment + with + member _.Dispose() = failwith "-" + } + """ + testCaseAsync "new on prev line" <| + CodeFix.check server + """ + open System + { + new + IDis$0posable with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new + IDisposable with + member _.Dispose() = failwith "-" + } + """ + testCaseAsync "new and with on extra lines" <| + CodeFix.check server + """ + open System + { + new + IDis$0posable + with + } + """ + validateDiags + selectCodeFixWithoutTypeAnnotation + """ + open System + { + new + IDisposable + with + member _.Dispose() = failwith "-" + } + """ + ] + ] + ]) + let config = { + defaultConfigDto with + IndentationSize = Some 6 + InterfaceStubGeneration = Some true + InterfaceStubGenerationObjectIdentifier = Some "this" + InterfaceStubGenerationMethodBody = Some "raise (System.NotImplementedException())" + } + serverTestList "with 6 indentation" state config None (fun server -> [ + let testBoth = testBoth server + testBoth "uses indentation, object identifier & method body from config" + """ + type X() = + interface System.$0IDisposable with + """ + """ + type X() = + interface System.IDisposable with + member this.Dispose(): unit = + raise (System.NotImplementedException()) + """ + """ + type X() = + interface System.IDisposable with + member this.Dispose() = raise (System.NotImplementedException()) + """ + () + ]) + ] diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs new file mode 100644 index 000000000..a8f8b69d3 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/RenameParamToMatchSignatureTests.fs @@ -0,0 +1,338 @@ +module private FsAutoComplete.Tests.CodeFixTests.RenameParamToMatchSignatureTests + +open Expecto +open Helpers +open System.IO +open Utils.Utils +open Utils.TextEdit +open Utils.ServerTests +open Utils.CursorbasedTests +open FsAutoComplete.CodeFix +open Utils.Server +open Utils.CursorbasedTests.CodeFix + + +let tests state = + let selectCodeFix expectedName = CodeFix.withTitle (RenameParamToMatchSignature.title expectedName) + + // requires `fsi` and corresponding `fs` file (and a project!) + // -> cannot use untitled doc + // -> use existing files, but load with text specified in tests + let path = Path.Combine(__SOURCE_DIRECTORY__, @"../TestCases/CodeFixTests/RenameParamToMatchSignature/") + let (fsiFile, fsFile) = ("Code.fsi", "Code.fs") + let (fsiPath, fsPath) = (Path.Combine(path, fsiFile), Path.Combine(path, fsFile)) + + serverTestList (nameof RenameParamToMatchSignature) state defaultConfigDto (Some path) (fun server -> [ + let checkWithFsi + fsiSource + fsSourceWithCursor + selectCodeFix + fsSourceExpected + = async { + let fsiSource = fsiSource |> Text.trimTripleQuotation + let (cursor, fsSource) = + fsSourceWithCursor + |> Text.trimTripleQuotation + |> Cursor.assertExtractRange + let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource + use fsiDoc = fsiDoc + Expect.isEmpty diags "There should be no diagnostics in fsi doc" + let! (fsDoc, diags) = server |> Server.openDocumentWithText fsFile fsSource + use fsDoc = fsDoc + + do! + checkFixAt + (fsDoc, diags) + (fsSource, cursor) + (Diagnostics.expectCode "3218") + selectCodeFix + (After (fsSourceExpected |> Text.trimTripleQuotation)) + } + + testCaseAsync "can rename parameter in F# function" <| + checkWithFsi + """ + module Code + + val f: value: int -> int + """ + """ + module Code + + let f $0v = v + 1 + """ + (selectCodeFix "value") + """ + module Code + + let f value = value + 1 + """ + testCaseAsync "can rename parameter with backticks in signature in F# function" <| + checkWithFsi + """ + module Code + + val f: ``my value``: int -> int + """ + """ + module Code + + let f $0v = v + 1 + """ + (selectCodeFix "``my value``") + """ + module Code + + let f ``my value`` = ``my value`` + 1 + """ + testCaseAsync "can rename parameter with backticks in implementation in F# function" <| + checkWithFsi + """ + module Code + + val f: value: int -> int + """ + """ + module Code + + let f ``$0my value`` = ``my value`` + 1 + """ + (selectCodeFix "value") + """ + module Code + + let f value = value + 1 + """ + testCaseAsync "can rename all usage in F# function" <| + checkWithFsi + """ + module Code + + val f: x: int -> value: int -> y: int -> int + """ + """ + module Code + + let f x $0v y = + let a = v + 1 + let b = v * v + let v = a + b + v + x * y + """ + (selectCodeFix "value") + """ + module Code + + let f x value y = + let a = value + 1 + let b = value * value + let v = a + b + v + x * y + """ + testCaseAsync "can rename parameter with type in F# function" <| + checkWithFsi + """ + module Code + + val f: value: int -> int + """ + """ + module Code + + let f ($0v: int) = v + 1 + """ + (selectCodeFix "value") + """ + module Code + + let f (value: int) = value + 1 + """ + testCaseAsync "can rename parameter in constructor" <| + checkWithFsi + """ + module Code + + type T = + new: value: int -> T + """ + """ + module Code + + type T($0v: int) = + let _ = v + 3 + """ + (selectCodeFix "value") + """ + module Code + + type T(value: int) = + let _ = value + 3 + """ + testCaseAsync "can rename parameter in member" <| + checkWithFsi + """ + module Code + + type T = + new: unit -> T + member F: value: int -> int + """ + """ + module Code + + type T() = + member _.F($0v) = v + 1 + """ + (selectCodeFix "value") + """ + module Code + + type T() = + member _.F(value) = value + 1 + """ + testCaseAsync "can rename parameter with ' in signature in F# function" <| + checkWithFsi + """ + module Code + + val f: value': int -> int + """ + """ + module Code + + let f $0v = v + 1 + """ + (selectCodeFix "value'") + """ + module Code + + let f value' = value' + 1 + """ + testCaseAsync "can rename parameter with ' in implementation in F# function" <| + checkWithFsi + """ + module Code + + val f: value: int -> int + """ + """ + module Code + + let f $0v' = v' + 1 + """ + (selectCodeFix "value") + """ + module Code + + let f value = value + 1 + """ + testCaseAsync "can rename parameter with ' (not in last place) in signature in F# function" <| + checkWithFsi + """ + module Code + + val f: v'2: int -> int + """ + """ + module Code + + let f $0value = value + 1 + """ + (selectCodeFix "v'2") + """ + module Code + + let f v'2 = v'2 + 1 + """ + testCaseAsync "can rename parameter with ' (not in last place) in implementation in F# function" <| + checkWithFsi + """ + module Code + + val f: value: int -> int + """ + """ + module Code + + let f $0v'2 = v'2 + 1 + """ + (selectCodeFix "value") + """ + module Code + + let f value = value + 1 + """ + testCaseAsync "can rename parameter with multiple ' in signature in F# function" <| + checkWithFsi + """ + module Code + + val f: value'v'2: int -> int + """ + """ + module Code + + let f $0v = v + 1 + """ + (selectCodeFix "value'v'2") + """ + module Code + + let f value'v'2 = value'v'2 + 1 + """ + testCaseAsync "can rename parameter with multiple ' in implementation in F# function" <| + checkWithFsi + """ + module Code + + val f: value: int -> int + """ + """ + module Code + + let f $0value'v'2 = value'v'2 + 1 + """ + (selectCodeFix "value") + """ + module Code + + let f value = value + 1 + """ + itestCaseAsync "can handle `' and implementation '` in impl name" <| + checkWithFsi + """ + module Code + + val f: value: int -> int + """ + """ + module Code + + let f $0``sig' and implementation 'impl' do not match`` = ``sig' and implementation 'impl' do not match`` + 1 + """ + (selectCodeFix "value") + """ + module Code + + let f value = value + 1 + """ + //ENHANCEMENT: correctly detect below. Currently: detects sig name `sig` + itestCaseAsync "can handle `' and implementation '` in sig name" <| + checkWithFsi + """ + module Code + + val f: ``sig' and implementation 'impl' do not match``: int -> int + """ + """ + module Code + + let f $0value = value + 1 + """ + (selectCodeFix "``sig' and implementation 'impl' do not match``") + """ + module Code + + let f ``sig' and implementation 'impl' do not match`` = ``sig' and implementation 'impl' do not match`` + 1 + """ + ]) \ No newline at end of file diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs similarity index 63% rename from test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs rename to test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs index 320f69a16..3b779ea7e 100644 --- a/test/FsAutoComplete.Tests.Lsp/CodeFixTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs @@ -1,419 +1,11 @@ -module FsAutoComplete.Tests.CodeFixTests +module FsAutoComplete.Tests.CodeFixTests.Tests open Expecto open Helpers -open System.IO -open Utils.Utils -open Utils.TextEdit open Utils.ServerTests open Utils.CursorbasedTests open Ionide.LanguageServerProtocol.Types open FsAutoComplete.CodeFix -open Utils.Server -open Utils.CursorbasedTests.CodeFix - -module private Diagnostics = - let expectCode code (diags: Diagnostic[]) = - Expecto.Flip.Expect.exists - $"There should be a Diagnostic with code %s{code}" - (fun (d: Diagnostic) -> d.Code = Some code) - diags - let acceptAll = ignore - - open FsAutoComplete.Logging - let private logger = FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.Diagnostics" - /// Usage: `(Diagnostics.log >> Diagnostics.expectCode "XXX")` - /// Logs as `info` - let log (diags: Diagnostic[]) = - logger.info ( - Log.setMessage "diags({count})={diags}" - >> Log.addContext "count" diags.Length - >> Log.addContextDestructured "diags" diags - ) - diags - -module CodeFix = - open FsAutoComplete.Logging - let private logger = FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.CodeFix" - /// Usage: `(CodeFix.log >> CodeFix.withTitle "XXX")` - /// Logs as `info` - let log (codeActions: CodeAction[]) = - logger.info ( - Log.setMessage "codeActions({count})={codeActions}" - >> Log.addContext "count" codeActions.Length - >> Log.addContextDestructured "codeActions" codeActions - ) - codeActions - -/// `ignore testCaseAsync` -/// -/// Like `testCaseAsync`, but test gets completely ignored. -/// Unlike `ptestCaseAsync` (pending), this here doesn't even show up in Expecto summary. -/// -/// -> Used to mark issues & shortcomings in CodeFixes, but without any (immediate) intention to fix -/// (vs. `pending` -> marked for fixing) -/// -> ~ uncommenting tests without actual uncommenting -let itestCaseAsync name test = () - -let private addExplicitTypeToParameterTests state = - serverTestList (nameof AddExplicitTypeToParameter) state defaultConfigDto None (fun server -> [ - let selectCodeFix = CodeFix.withTitle AddExplicitTypeToParameter.title - testCaseAsync "can suggest explicit parameter for record-typed function parameters" <| - CodeFix.check server - """ - type Foo = - { name: string } - - let name $0f = - f.name - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - type Foo = - { name: string } - - let name (f: Foo) = - f.name - """ - testCaseAsync "can add type for int param" <| - CodeFix.check server - """ - let f ($0x) = x + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x: int) = x + 1 - """ - testCaseAsync "can add type for generic param" <| - CodeFix.check server - """ - let f ($0x) = () - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x: 'a) = () - """ - testCaseAsync "doesn't trigger when existing type" <| - CodeFix.checkNotApplicable server - """ - let f ($0x: int) = () - """ - (Diagnostics.acceptAll) - selectCodeFix - testCaseAsync "can add type to tuple item" <| - CodeFix.check server - """ - let f (a, $0b, c) = a + b + c + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (a, b: int, c) = a + b + c + 1 - """ - testCaseAsync "doesn't trigger in tuple when existing type" <| - CodeFix.checkNotApplicable server - """ - let f (a, $0b: int, c) = a + b + c + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - testCaseAsync "can add type to 2nd of 3 param" <| - CodeFix.check server - """ - let f a $0b c = a + b + c + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f a (b: int) c = a + b + c + 1 - """ - testCaseAsync "doesn't trigger on 2nd of 3 param when existing type" <| - CodeFix.checkNotApplicable server - """ - let f a ($0b: int) c = a + b + c + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - testCaseAsync "can add type to 2nd of 3 param when other params have types" <| - CodeFix.check server - """ - let f (a: int) $0b (c: int) = a + b + c + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (a: int) (b: int) (c: int) = a + b + c + 1 - """ - testCaseAsync "can add type to member param" <| - CodeFix.check server - """ - type A() = - member _.F($0a) = a + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - type A() = - member _.F(a: int) = a + 1 - """ - testCaseAsync "doesn't trigger for member param when existing type" <| - CodeFix.checkNotApplicable server - """ - type A() = - member _.F($0a: int) = a + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - testCaseAsync "can add type to ctor param" <| - CodeFix.check server - """ - type A($0a) = - member _.F() = a + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - type A(a: int) = - member _.F() = a + 1 - """ - testCaseAsync "doesn't trigger for ctor param when existing type" <| - CodeFix.checkNotApplicable server - """ - type A($0a: int) = - member _.F() = a + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - testCaseAsync "can add type to correct ctor param" <| - CodeFix.check server - """ - type A(str, $0n, b) = - member _.FString() = sprintf "str=%s" str - member _.FInt() = n + 1 - member _.FBool() = sprintf "b=%b" b - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - type A(str, n: int, b) = - member _.FString() = sprintf "str=%s" str - member _.FInt() = n + 1 - member _.FBool() = sprintf "b=%b" b - """ - testCaseAsync "doesn't trigger for ctor param when existing type and multiple params" <| - CodeFix.checkNotApplicable server - """ - type A(str, $0n: int, b) = - member _.FString() = sprintf "str=%s" str - member _.FInt() = a + 1 - member _.FBool() = sprintf "b=%b" b - """ - (Diagnostics.acceptAll) - selectCodeFix - testCaseAsync "can add type to secondary ctor param" <| - CodeFix.check server - """ - type A(a) = - new($0a, b) = A(a+b) - member _.F() = a + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - type A(a) = - new(a: int, b) = A(a+b) - member _.F() = a + 1 - """ - testList "parens" [ - testCaseAsync "single param without parens -> add parens" <| - CodeFix.check server - """ - let f $0x = x + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x: int) = x + 1 - """ - testCaseAsync "single param with parens -> keep parens" <| - CodeFix.check server - """ - let f ($0x) = x + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x: int) = x + 1 - """ - testCaseAsync "multi params without parens -> add parens" <| - CodeFix.check server - """ - let f a $0x y = x + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f a (x: int) y = x + 1 - """ - testCaseAsync "multi params with parens -> keep parens" <| - CodeFix.check server - """ - let f a ($0x) y = x + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f a (x: int) y = x + 1 - """ - testList "tuple params without parens -> no parens" [ - testCaseAsync "start" <| - CodeFix.check server - """ - let f ($0x, y, z) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x: int, y, z) = x + y + z + 1 - """ - testCaseAsync "center" <| - CodeFix.check server - """ - let f (x, $0y, z) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x, y: int, z) = x + y + z + 1 - """ - testCaseAsync "end" <| - CodeFix.check server - """ - let f (x, y, $0z) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x, y, z: int) = x + y + z + 1 - """ - ] - testList "tuple params with parens -> keep parens" [ - testCaseAsync "start" <| - CodeFix.check server - """ - let f (($0x), y, z) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f ((x: int), y, z) = x + y + z + 1 - """ - testCaseAsync "center" <| - CodeFix.check server - """ - let f (x, ($0y), z) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x, (y: int), z) = x + y + z + 1 - """ - testCaseAsync "end" <| - CodeFix.check server - """ - let f (x, y, ($0z)) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f (x, y, (z: int)) = x + y + z + 1 - """ - ] - testList "tuple params without parens but spaces -> no parens" [ - testCaseAsync "start" <| - CodeFix.check server - """ - let f ( $0x , y , z ) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f ( x: int , y , z ) = x + y + z + 1 - """ - testCaseAsync "center" <| - CodeFix.check server - """ - let f ( x , $0y , z ) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f ( x , y: int , z ) = x + y + z + 1 - """ - testCaseAsync "end" <| - CodeFix.check server - """ - let f ( x , y , $0z ) = x + y + z + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f ( x , y , z: int ) = x + y + z + 1 - """ - ] - testList "long tuple params without parens but spaces -> no parens" [ - testCaseAsync "start" <| - CodeFix.check server - """ - let f ( xV$0alue , yAnotherValue , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f ( xValue: int , yAnotherValue , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 - """ - testCaseAsync "center" <| - CodeFix.check server - """ - let f ( xValue , yAn$0otherValue , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f ( xValue , yAnotherValue: int , zFinalValue ) = xValue + yAnotherValue + zFinalValue + 1 - """ - testCaseAsync "end" <| - CodeFix.check server - """ - let f ( xValue , yAnotherValue , zFina$0lValue ) = xValue + yAnotherValue + zFinalValue + 1 - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - let f ( xValue , yAnotherValue , zFinalValue: int ) = xValue + yAnotherValue + zFinalValue + 1 - """ - ] - testCaseAsync "never add parens to primary ctor param" <| - CodeFix.check server - """ - type A ( - $0a - ) = - member _.F(b) = a + b - """ - (Diagnostics.acceptAll) - selectCodeFix - """ - type A ( - a: int - ) = - member _.F(b) = a + b - """ - ] - ]) let private addMissingEqualsToTypeDefinitionTests state = serverTestList (nameof AddMissingEqualsToTypeDefinition) state defaultConfigDto None (fun server -> [ @@ -1620,331 +1212,6 @@ let private removeUnusedOpensTests state = selectCodeFix ]) -let private renameParamToMatchSignatureTests state = - let selectCodeFix expectedName = CodeFix.withTitle (RenameParamToMatchSignature.title expectedName) - - // requires `fsi` and corresponding `fs` file (and a project!) - // -> cannot use untitled doc - // -> use existing files, but load with text specified in tests - let path = Path.Combine(__SOURCE_DIRECTORY__, @"./TestCases/CodeFixTests/RenameParamToMatchSignature/") - let (fsiFile, fsFile) = ("Code.fsi", "Code.fs") - let (fsiPath, fsPath) = (Path.Combine(path, fsiFile), Path.Combine(path, fsFile)) - - serverTestList (nameof RenameParamToMatchSignature) state defaultConfigDto (Some path) (fun server -> [ - let checkWithFsi - fsiSource - fsSourceWithCursor - selectCodeFix - fsSourceExpected - = async { - let fsiSource = fsiSource |> Text.trimTripleQuotation - let (cursor, fsSource) = - fsSourceWithCursor - |> Text.trimTripleQuotation - |> Cursor.assertExtractRange - let! (fsiDoc, diags) = server |> Server.openDocumentWithText fsiFile fsiSource - use fsiDoc = fsiDoc - Expect.isEmpty diags "There should be no diagnostics in fsi doc" - let! (fsDoc, diags) = server |> Server.openDocumentWithText fsFile fsSource - use fsDoc = fsDoc - - do! - checkFixAt - (fsDoc, diags) - (fsSource, cursor) - (Diagnostics.expectCode "3218") - selectCodeFix - (After (fsSourceExpected |> Text.trimTripleQuotation)) - } - - testCaseAsync "can rename parameter in F# function" <| - checkWithFsi - """ - module Code - - val f: value: int -> int - """ - """ - module Code - - let f $0v = v + 1 - """ - (selectCodeFix "value") - """ - module Code - - let f value = value + 1 - """ - testCaseAsync "can rename parameter with backticks in signature in F# function" <| - checkWithFsi - """ - module Code - - val f: ``my value``: int -> int - """ - """ - module Code - - let f $0v = v + 1 - """ - (selectCodeFix "``my value``") - """ - module Code - - let f ``my value`` = ``my value`` + 1 - """ - testCaseAsync "can rename parameter with backticks in implementation in F# function" <| - checkWithFsi - """ - module Code - - val f: value: int -> int - """ - """ - module Code - - let f ``$0my value`` = ``my value`` + 1 - """ - (selectCodeFix "value") - """ - module Code - - let f value = value + 1 - """ - testCaseAsync "can rename all usage in F# function" <| - checkWithFsi - """ - module Code - - val f: x: int -> value: int -> y: int -> int - """ - """ - module Code - - let f x $0v y = - let a = v + 1 - let b = v * v - let v = a + b - v + x * y - """ - (selectCodeFix "value") - """ - module Code - - let f x value y = - let a = value + 1 - let b = value * value - let v = a + b - v + x * y - """ - testCaseAsync "can rename parameter with type in F# function" <| - checkWithFsi - """ - module Code - - val f: value: int -> int - """ - """ - module Code - - let f ($0v: int) = v + 1 - """ - (selectCodeFix "value") - """ - module Code - - let f (value: int) = value + 1 - """ - testCaseAsync "can rename parameter in constructor" <| - checkWithFsi - """ - module Code - - type T = - new: value: int -> T - """ - """ - module Code - - type T($0v: int) = - let _ = v + 3 - """ - (selectCodeFix "value") - """ - module Code - - type T(value: int) = - let _ = value + 3 - """ - testCaseAsync "can rename parameter in member" <| - checkWithFsi - """ - module Code - - type T = - new: unit -> T - member F: value: int -> int - """ - """ - module Code - - type T() = - member _.F($0v) = v + 1 - """ - (selectCodeFix "value") - """ - module Code - - type T() = - member _.F(value) = value + 1 - """ - testCaseAsync "can rename parameter with ' in signature in F# function" <| - checkWithFsi - """ - module Code - - val f: value': int -> int - """ - """ - module Code - - let f $0v = v + 1 - """ - (selectCodeFix "value'") - """ - module Code - - let f value' = value' + 1 - """ - testCaseAsync "can rename parameter with ' in implementation in F# function" <| - checkWithFsi - """ - module Code - - val f: value: int -> int - """ - """ - module Code - - let f $0v' = v' + 1 - """ - (selectCodeFix "value") - """ - module Code - - let f value = value + 1 - """ - testCaseAsync "can rename parameter with ' (not in last place) in signature in F# function" <| - checkWithFsi - """ - module Code - - val f: v'2: int -> int - """ - """ - module Code - - let f $0value = value + 1 - """ - (selectCodeFix "v'2") - """ - module Code - - let f v'2 = v'2 + 1 - """ - testCaseAsync "can rename parameter with ' (not in last place) in implementation in F# function" <| - checkWithFsi - """ - module Code - - val f: value: int -> int - """ - """ - module Code - - let f $0v'2 = v'2 + 1 - """ - (selectCodeFix "value") - """ - module Code - - let f value = value + 1 - """ - testCaseAsync "can rename parameter with multiple ' in signature in F# function" <| - checkWithFsi - """ - module Code - - val f: value'v'2: int -> int - """ - """ - module Code - - let f $0v = v + 1 - """ - (selectCodeFix "value'v'2") - """ - module Code - - let f value'v'2 = value'v'2 + 1 - """ - testCaseAsync "can rename parameter with multiple ' in implementation in F# function" <| - checkWithFsi - """ - module Code - - val f: value: int -> int - """ - """ - module Code - - let f $0value'v'2 = value'v'2 + 1 - """ - (selectCodeFix "value") - """ - module Code - - let f value = value + 1 - """ - itestCaseAsync "can handle `' and implementation '` in impl name" <| - checkWithFsi - """ - module Code - - val f: value: int -> int - """ - """ - module Code - - let f $0``sig' and implementation 'impl' do not match`` = ``sig' and implementation 'impl' do not match`` + 1 - """ - (selectCodeFix "value") - """ - module Code - - let f value = value + 1 - """ - //ENHANCEMENT: correctly detect below. Currently: detects sig name `sig` - itestCaseAsync "can handle `' and implementation '` in sig name" <| - checkWithFsi - """ - module Code - - val f: ``sig' and implementation 'impl' do not match``: int -> int - """ - """ - module Code - - let f $0value = value + 1 - """ - (selectCodeFix "``sig' and implementation 'impl' do not match``") - """ - module Code - - let f ``sig' and implementation 'impl' do not match`` = ``sig' and implementation 'impl' do not match`` + 1 - """ - ]) - let private renameUnusedValue state = let config = { defaultConfigDto with UnusedDeclarationsAnalyzer = Some true } serverTestList (nameof RenameUnusedValue) state config None (fun server -> [ @@ -2213,171 +1480,10 @@ let private wrapExpressionInParenthesesTests state = selectCodeFix ]) -/// Helper functions for CodeFixes -module private CodeFixHelpers = - // `src\FsAutoComplete\CodeFixes.fs` -> `FsAutoComplete.CodeFix` - open Navigation - open FSharp.Compiler.Text - - 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 + HelpersTests.tests - addExplicitTypeToParameterTests state + AddExplicitTypeToParameterTests.tests state addMissingEqualsToTypeDefinitionTests state addMissingFunKeywordTests state addMissingInstanceMemberTests state @@ -2398,13 +1504,14 @@ let tests state = testList "CodeFix tests" [ generateAbstractClassStubTests state generateRecordStubTests state generateUnionCasesTests state + ImplementInterfaceTests.tests state makeDeclarationMutableTests state makeOuterBindingRecursiveTests state removeRedundantQualifierTests state removeUnnecessaryReturnOrYieldTests state removeUnusedBindingTests state removeUnusedOpensTests state - renameParamToMatchSignatureTests state + RenameParamToMatchSignatureTests.tests state renameUnusedValue state replaceWithSuggestionTests state resolveNamespaceTests state diff --git a/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs new file mode 100644 index 000000000..b72087ee3 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/CodeFixTests/Utils.fs @@ -0,0 +1,46 @@ +[] +module private FsAutoComplete.Tests.CodeFixTests.Utils + +open Ionide.LanguageServerProtocol.Types +open FsAutoComplete.Logging + +module Diagnostics = + let expectCode code (diags: Diagnostic[]) = + Expecto.Flip.Expect.exists + $"There should be a Diagnostic with code %s{code}" + (fun (d: Diagnostic) -> d.Code = Some code) + diags + let acceptAll = ignore + + let private logger = FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.Diagnostics" + /// Usage: `(Diagnostics.log >> Diagnostics.expectCode "XXX")` + /// Logs as `info` + let log (diags: Diagnostic[]) = + logger.info ( + Log.setMessage "diags({count})={diags}" + >> Log.addContext "count" diags.Length + >> Log.addContextDestructured "diags" diags + ) + diags + +module CodeFix = + let private logger = FsAutoComplete.Logging.LogProvider.getLoggerByName "CodeFixes.CodeFix" + /// Usage: `(CodeFix.log >> CodeFix.withTitle "XXX")` + /// Logs as `info` + let log (codeActions: CodeAction[]) = + logger.info ( + Log.setMessage "codeActions({count})={codeActions}" + >> Log.addContext "count" codeActions.Length + >> Log.addContextDestructured "codeActions" codeActions + ) + codeActions + +/// `ignore testCaseAsync` +/// +/// Like `testCaseAsync`, but test gets completely ignored. +/// Unlike `ptestCaseAsync` (pending), this here doesn't even show up in Expecto summary. +/// +/// -> Used to mark issues & shortcomings in CodeFixes, but without any (immediate) intention to fix +/// (vs. `pending` -> marked for fixing) +/// -> ~ uncommenting tests without actual uncommenting +let itestCaseAsync name test = () diff --git a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj index d0090d26c..bdd3c2f82 100644 --- a/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj +++ b/test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj @@ -27,6 +27,9 @@ + + + diff --git a/test/FsAutoComplete.Tests.Lsp/Helpers.fs b/test/FsAutoComplete.Tests.Lsp/Helpers.fs index 74ab1694d..dad39485e 100644 --- a/test/FsAutoComplete.Tests.Lsp/Helpers.fs +++ b/test/FsAutoComplete.Tests.Lsp/Helpers.fs @@ -176,6 +176,7 @@ let defaultConfigDto: FSharpConfigDto = ExternalAutocomplete = None Linter = None LinterConfig = None + IndentationSize = None UnionCaseStubGeneration = None UnionCaseStubGenerationBody = None RecordStubGeneration = None diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 305d6906e..30e508805 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -77,7 +77,7 @@ let lspTests = analyzerTests state signatureTests state SignatureHelp.tests state - CodeFixTests.tests state + CodeFixTests.Tests.tests state Completion.tests state GoTo.tests state FindReferences.tests state