diff --git a/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs index d27835923f..49f46d0891 100644 --- a/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs +++ b/vsintegration/src/FSharp.Editor/CodeFix/ImplementInterfaceCodeFixProvider.fs @@ -23,6 +23,7 @@ open Microsoft.FSharp.Compiler.SourceCodeServices type internal InterfaceState = { InterfaceData: InterfaceData EndPosOfWith: pos option + AppendBracketAt: int option Tokens: FSharpTokenInfo list } [] @@ -36,7 +37,7 @@ type internal FSharpImplementInterfaceCodeFixProvider let fixableDiagnosticIds = ["FS0366"] let checker = checkerProvider.Checker - let queryInterfaceState (pos: pos) tokens (ast: Ast.ParsedInput) = + let queryInterfaceState appendBracketAt (pos: pos) tokens (ast: Ast.ParsedInput) = async { let line = pos.Line - 1 let column = pos.Column @@ -49,7 +50,11 @@ type internal FSharpImplementInterfaceCodeFixProvider if t.CharClass = FSharpTokenCharKind.Keyword && t.LeftColumn >= column && t.TokenName = "WITH" then Some (Pos.fromZ line (t.RightColumn + 1)) else None) - return Some { InterfaceData = iface; EndPosOfWith = endPosOfWidth; Tokens = tokens } + let appendBracketAt = + match iface, appendBracketAt with + | InterfaceData.ObjExpr _, Some _ -> appendBracketAt + | _ -> None + return Some { InterfaceData = iface; EndPosOfWith = endPosOfWidth; AppendBracketAt = appendBracketAt; Tokens = tokens } } let getLineIdent (lineStr: string) = @@ -75,7 +80,7 @@ type internal FSharpImplementInterfaceCodeFixProvider // There is no reference point, we indent the content at the start column of the interface |> Option.defaultValue iface.Range.StartColumn - let handleImplementInterface (sourceText: SourceText) state displayContext implementedMemberSignatures entity indentSize verboseMode = + let applyImplementInterface (sourceText: SourceText) state displayContext implementedMemberSignatures entity indentSize verboseMode = let startColumn = inferStartColumn indentSize state sourceText let objectIdentifier = "this" let defaultBody = "raise (System.NotImplementedException())" @@ -85,15 +90,21 @@ type internal FSharpImplementInterfaceCodeFixProvider startColumn indentSize typeParams objectIdentifier defaultBody displayContext implementedMemberSignatures entity verboseMode stub.TrimEnd(Environment.NewLine.ToCharArray()) - match state.EndPosOfWith with - | Some pos -> - let currentPos = sourceText.Lines.[pos.Line-1].Start + pos.Column - TextChange(TextSpan(currentPos, 0), stub) + let stubChange = + match state.EndPosOfWith with + | Some pos -> + let currentPos = sourceText.Lines.[pos.Line-1].Start + pos.Column + TextChange(TextSpan(currentPos, 0), stub) + | None -> + let range = state.InterfaceData.Range + let currentPos = sourceText.Lines.[range.EndLine-1].Start + range.EndColumn + TextChange(TextSpan(currentPos, 0), " with" + stub) + match state.AppendBracketAt with + | Some index -> + sourceText.WithChanges(stubChange, TextChange(TextSpan(index, 0), " }")) | None -> - let range = state.InterfaceData.Range - let currentPos = sourceText.Lines.[range.EndLine-1].Start + range.EndColumn - TextChange(TextSpan(currentPos, 0), " with" + stub) - + sourceText.WithChanges(stubChange) + let registerSuggestions (context: CodeFixContext, results: FSharpCheckFileResults, state: InterfaceState, displayContext, entity, indentSize) = if InterfaceStubGenerator.hasNoInterfaceMember entity then () @@ -116,8 +127,8 @@ type internal FSharpImplementInterfaceCodeFixProvider results.GetSymbolUseAtLocation(range.EndLine, range.EndColumn, lineStr, [name]) let! implementedMemberSignatures = InterfaceStubGenerator.getImplementedMemberSignatures getMemberByLocation displayContext state.InterfaceData - let textChange = handleImplementInterface sourceText state displayContext implementedMemberSignatures entity indentSize verboseMode - return context.Document.WithText(sourceText.WithChanges textChange) + let newSourceText = applyImplementInterface sourceText state displayContext implementedMemberSignatures entity indentSize verboseMode + return context.Document.WithText(newSourceText) } |> CommonRoslynHelpers.StartAsyncAsTask(cancellationToken)), title) context.RegisterCodeFix(codeAction, diagnostics) @@ -146,8 +157,12 @@ type internal FSharpImplementInterfaceCodeFixProvider // Notice that context.Span doesn't return reliable ranges to find tokens at exact positions. // That's why we tokenize the line and try to find the last successive identifier token let tokens = CommonHelpers.tokenizeLine(context.Document.Id, sourceText, context.Span.Start, context.Document.FilePath, defines) + let startLeftColumn = context.Span.Start - textLine.Start let rec tryFindIdentifierToken acc tokens = match tokens with + | t :: remainingTokens when t.LeftColumn < startLeftColumn -> + // Skip all the tokens starting before the context + tryFindIdentifierToken acc remainingTokens | t :: remainingTokens when t.Tag = FSharpTokenTag.Identifier -> tryFindIdentifierToken (Some t) remainingTokens | t :: remainingTokens when t.Tag = FSharpTokenTag.DOT || Option.isNone acc -> @@ -158,7 +173,13 @@ type internal FSharpImplementInterfaceCodeFixProvider | Some token -> let fixupPosition = textLine.Start + token.RightColumn let interfacePos = Pos.fromZ textLine.LineNumber token.RightColumn - let! interfaceState = queryInterfaceState interfacePos tokens parsedInput + // We rely on the observation that the lastChar of the context should be '}' if that character is present + let appendBracketAt = + match sourceText.[context.Span.End-1] with + | '}' -> None + | _ -> + Some context.Span.End + let! interfaceState = queryInterfaceState appendBracketAt interfacePos tokens parsedInput let symbol = CommonHelpers.getSymbolAtPosition(context.Document.Id, sourceText, fixupPosition, context.Document.FilePath, defines, SymbolLookupKind.Fuzzy) match interfaceState, symbol with | Some state, Some symbol ->