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