Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 24 additions & 39 deletions build/ScaffoldCodeFix.fs
Original file line number Diff line number Diff line change
Expand Up @@ -38,48 +38,13 @@ let mkCodeFixImplementation codeFixName =

open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FsToolkit.ErrorHandling
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers

// The syntax tree can be an intimidating set of types to work with.
// It is a tree structure but it consists out of many different types.
// See https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-syntax.html
// It can be useful to inspect a syntax tree via a code sample using https://fsprojects.github.io/fantomas-tools/#/ast
// For example `let a b c = ()` in
// https://fsprojects.github.io/fantomas-tools/#/ast?data=N4KABGBEAmCmBmBLAdrAzpAXFSAacUiaAYmolmPAIYA2as%%2BEkAxgPZwWQ2wAuYVYAEZhmYALxgAFAEo8BSLAAeAByrJoFHgCcArrBABfIA
// Let's say we want to find the (FCS) range for identifier `a`.
let visitSyntaxTree
(cursor: FSharp.Compiler.Text.pos)
(tree: ParsedInput)
=
// We will use a syntax visitor to traverse the tree from the top to the node of interest.
// See https://github.com/dotnet/fsharp/blob/main/src/Compiler/Service/ServiceParseTreeWalk.fsi
// We implement the different members of interest and allow the default traversal to move to the lower levels we care about.
let visitor =
// A visitor will report the first item it finds.
// Think of it as `List.tryPick`
// It is not an ideal solution to find all nodes inside a tree, be aware of that.
// For example finding all function names.
{{ new SyntaxVisitorBase<FSharp.Compiler.Text.range>() with
// We know that `a` will be part of a `SynPat.LongIdent`
// This was visible in the online tool.
member _.VisitPat(path, defaultTraverse, synPat) =
match synPat with
| SynPat.LongIdent(longDotId = SynLongIdent(id = [ functionNameIdent ])) ->
// When our code fix operates on the user's code there is no way of knowing what will be inside the syntax tree.
// So we need to be careful and verify that the pattern is indeed matching the position of the cursor.
if FSharp.Compiler.Text.Range.rangeContainsPos functionNameIdent.idRange cursor then
Some functionNameIdent.idRange
else
None
| _ -> None }}

// Invoke the visitor and kick off the traversal.
SyntaxTraversal.Traverse(cursor, tree, visitor)

// TODO: add proper title for code fix
let title = "%s{codeFixName} Codefix"

Expand All @@ -98,9 +63,29 @@ let fix
let! (parseAndCheckResults:ParseAndCheckResults, line:string, sourceText:IFSACSourceText) =
getParseResultsForFile fileName fcsPos

// As an example, we want to check whether the users cursor is inside a function definition name.
// We will traverse the syntax tree to verify this is the case.
match visitSyntaxTree fcsPos parseAndCheckResults.GetParseResults.ParseTree with
// The syntax tree can be an intimidating set of types to work with.
// It is a tree structure but it consists out of many different types.
// See https://fsharp.github.io/fsharp-compiler-docs/reference/fsharp-compiler-syntax.html
// It can be useful to inspect a syntax tree via a code sample using https://fsprojects.github.io/fantomas-tools/#/ast
// For example `let a b c = ()` in
// https://fsprojects.github.io/fantomas-tools/#/ast?data=N4KABGBEAmCmBmBLAdrAzpAXFSAacUiaAYmolmPAIYA2as%%2BEkAxgPZwWQ2wAuYVYAEZhmYALxgAFAEo8BSLAAeAByrJoFHgCcArrBABfIA
// Let's say we want to find the (FCS) range for identifier `a` if the user's cursor is inside the function name.
// We will query the syntax tree to verify this is the case.
let maybeFunctionNameRange =
(fcsPos, parseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun path node ->
match node with
// We know that `a` will be part of a `SynPat.LongIdent`
// This was visible in the online tool.
| SyntaxNode.SynPat(SynPat.LongIdent(longDotId = SynLongIdent(id = [ functionNameIdent ]))) when
// When our code fix operates on the user's code there is no way of knowing what will be inside the syntax tree.
// So we need to be careful and verify that the pattern is indeed matching the position of the cursor.
Range.rangeContainsPos functionNameIdent.idRange fcsPos
->
Some functionNameIdent.idRange
| _ -> None)

match maybeFunctionNameRange with
| None ->
// The cursor is not in a position we are interested in.
// This code fix should not trigger any suggestions so we return an empty list.
Expand Down
33 changes: 10 additions & 23 deletions src/FsAutoComplete.Core/AbstractClassStubGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -71,28 +71,6 @@ let private walkTypeDefn (SynTypeDefn(_, repr, members, implicitCtor, _, _)) =

}

/// find the declaration of the abstract class being filled in at the given position
let private tryFindAbstractClassExprInParsedInput
(pos: Position)
(parsedInput: ParsedInput)
: AbstractClassData option =
SyntaxTraversal.Traverse(
pos,
parsedInput,
{ new SyntaxVisitorBase<_>() with
member _.VisitExpr(path, traverseExpr, defaultTraverse, expr) =
match expr with
| SynExpr.ObjExpr(
objType = baseTy; withKeyword = withKeyword; bindings = bindings; newExprRange = newExprRange) ->
Some(AbstractClassData.ObjExpr(baseTy, bindings, newExprRange, withKeyword))
| _ -> defaultTraverse expr

override _.VisitModuleDecl(_, defaultTraverse, decl) =
match decl with
| SynModuleDecl.Types(types, _) -> List.tryPick walkTypeDefn types
| _ -> defaultTraverse decl }
)

/// Walk the parse tree for the given document and look for the definition of any abstract classes in use at the given pos.
/// This looks for implementations of abstract types in object expressions, as well as inheriting of abstract types inside class type declarations.
let tryFindAbstractClassExprInBufferAtPos
Expand All @@ -102,7 +80,16 @@ let tryFindAbstractClassExprInBufferAtPos
=
asyncOption {
let! parseResults = codeGenService.ParseFileInProject document.FileName
return! tryFindAbstractClassExprInParsedInput pos parseResults.ParseTree

return!
(pos, parseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
match node with
| SyntaxNode.SynExpr(SynExpr.ObjExpr(
objType = baseTy; withKeyword = withKeyword; bindings = bindings; newExprRange = newExprRange)) ->
Some(ObjExpr(baseTy, bindings, newExprRange, withKeyword))
| SyntaxNode.SynModule(SynModuleDecl.Types(types, _)) -> List.tryPick walkTypeDefn types
| _ -> None)
}

let getMemberNameAndRanges abstractClassData =
Expand Down
228 changes: 94 additions & 134 deletions src/FsAutoComplete.Core/Commands.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1267,142 +1267,102 @@ type Commands() =
static member GenerateXmlDocumentation(tyRes: ParseAndCheckResults, triggerPosition: Position, lineStr: LineStr) =
asyncResult {
let longIdentContainsPos (longIdent: LongIdent) (pos: FSharp.Compiler.Text.pos) =
longIdent
|> List.tryFind (fun i -> rangeContainsPos i.idRange pos)
|> Option.isSome

let isLowerAstElemWithEmptyPreXmlDoc input pos =
SyntaxTraversal.Traverse(
pos,
input,
{ new SyntaxVisitorBase<_>() with
member _.VisitBinding(_, defaultTraverse, synBinding) =
match synBinding with
| SynBinding(xmlDoc = xmlDoc; valData = valData) as s when
rangeContainsPos s.RangeOfBindingWithoutRhs pos && xmlDoc.IsEmpty
->
match valData with
| SynValData(memberFlags = Some({ MemberKind = SynMemberKind.PropertyGet }))
| SynValData(memberFlags = Some({ MemberKind = SynMemberKind.PropertySet }))
| SynValData(memberFlags = Some({ MemberKind = SynMemberKind.PropertyGetSet })) -> None
| _ -> Some false
| _ -> defaultTraverse synBinding

member _.VisitComponentInfo(_, synComponentInfo) =
match synComponentInfo with
| SynComponentInfo(longId = longId; xmlDoc = xmlDoc) when
longIdentContainsPos longId pos && xmlDoc.IsEmpty
->
Some false
| _ -> None

member _.VisitRecordDefn(_, fields, _) =
let isInLine c =
match c with
| SynField(xmlDoc = xmlDoc; idOpt = Some ident) when
rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty
->
Some false
| _ -> None

fields |> List.tryPick isInLine

member _.VisitUnionDefn(_, cases, _) =
let isInLine c =
match c with
| SynUnionCase(xmlDoc = xmlDoc; ident = (SynIdent(ident = ident))) when
rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty
->
Some false
| _ -> None

cases |> List.tryPick isInLine

member _.VisitEnumDefn(_, cases, _) =
let isInLine b =
match b with
| SynEnumCase(xmlDoc = xmlDoc; ident = (SynIdent(ident = ident))) when
rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty
->
Some false
| _ -> None

cases |> List.tryPick isInLine

member _.VisitLetOrUse(_, _, defaultTraverse, bindings, _) =
let isInLine b =
match b with
| SynBinding(xmlDoc = xmlDoc) as s when
rangeContainsPos s.RangeOfBindingWithoutRhs pos && xmlDoc.IsEmpty
->
Some false
| _ -> defaultTraverse b

bindings |> List.tryPick isInLine

member _.VisitExpr(_, _, defaultTraverse, expr) = defaultTraverse expr } // needed for nested let bindings
)

let isModuleOrNamespaceOrAutoPropertyWithEmptyPreXmlDoc input pos =
SyntaxTraversal.Traverse(
pos,
input,
{ new SyntaxVisitorBase<_>() with

member _.VisitModuleOrNamespace(_, synModuleOrNamespace) =
match synModuleOrNamespace with
| SynModuleOrNamespace(longId = longId; xmlDoc = xmlDoc) when
longIdentContainsPos longId pos && xmlDoc.IsEmpty
->
Some false
| SynModuleOrNamespace(decls = decls) ->

let rec findNested decls =
decls
|> List.tryPick (fun d ->
match d with
| SynModuleDecl.NestedModule(moduleInfo = moduleInfo; decls = decls) ->
match moduleInfo with
| SynComponentInfo(longId = longId; xmlDoc = xmlDoc) when
longIdentContainsPos longId pos && xmlDoc.IsEmpty
->
Some false
| _ -> findNested decls
| SynModuleDecl.Types(typeDefns = typeDefns) ->
typeDefns
|> List.tryPick (fun td ->
match td with
| SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(_, members, _)) ->
members
|> List.tryPick (fun m ->
match m with
| SynMemberDefn.AutoProperty(ident = ident; xmlDoc = xmlDoc) when
rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty
->
Some true
| SynMemberDefn.GetSetMember(
memberDefnForSet = Some(SynBinding(
xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId)))) when
rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty
->
Some false
| SynMemberDefn.GetSetMember(
memberDefnForGet = Some(SynBinding(
xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId)))) when
rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty
->
Some false
| _ -> None)
| _ -> None)
| _ -> None)

findNested decls }
)
longIdent |> List.exists (fun i -> rangeContainsPos i.idRange pos)

let isAstElemWithEmptyPreXmlDoc input pos =
match isLowerAstElemWithEmptyPreXmlDoc input pos with
| Some isAutoProperty -> Some isAutoProperty
| _ -> isModuleOrNamespaceOrAutoPropertyWithEmptyPreXmlDoc input pos
(pos, input)
||> ParsedInput.tryPickLast (fun _path node ->
let (|AnyGetSetMemberInRange|_|) =
List.tryPick (function
| SynMemberDefn.GetSetMember(
memberDefnForSet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId))))
| SynMemberDefn.GetSetMember(
memberDefnForGet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId)))) when
rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty
->
Some()
| _ -> None)

match node with
| SyntaxNode.SynBinding(SynBinding(
valData = SynValData(Some { MemberKind = SynMemberKind.PropertyGet }, _, _)))
| SyntaxNode.SynBinding(SynBinding(
valData = SynValData(Some { MemberKind = SynMemberKind.PropertySet }, _, _)))
| SyntaxNode.SynBinding(SynBinding(
valData = SynValData(Some { MemberKind = SynMemberKind.PropertyGetSet }, _, _))) -> None

| SyntaxNode.SynBinding(SynBinding(xmlDoc = xmlDoc) as s) when
rangeContainsPos s.RangeOfBindingWithoutRhs pos && xmlDoc.IsEmpty
->
Some false

| SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(members = AnyGetSetMemberInRange))) ->
Some false

| SyntaxNode.SynTypeDefn(SynTypeDefn(typeInfo = SynComponentInfo(longId = longId; xmlDoc = xmlDoc))) when
longIdentContainsPos longId pos && xmlDoc.IsEmpty
->
Some false

| SyntaxNode.SynTypeDefn(SynTypeDefn(
typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record(recordFields = fields), _))) ->
let isInLine c =
match c with
| SynField(xmlDoc = xmlDoc; idOpt = Some ident) when rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty ->
Some false
| _ -> None

fields |> List.tryPick isInLine

| SyntaxNode.SynTypeDefn(SynTypeDefn(
typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Union(unionCases = cases), _))) ->
let isInLine c =
match c with
| SynUnionCase(xmlDoc = xmlDoc; ident = SynIdent(ident = ident)) when
rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty
->
Some false
| _ -> None

cases |> List.tryPick isInLine

| SyntaxNode.SynTypeDefn(SynTypeDefn(
typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Enum(cases = cases), _))) ->
let isInLine c =
match c with
| SynEnumCase(xmlDoc = xmlDoc; ident = SynIdent(ident = ident)) when
rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty
->
Some false
| _ -> None

cases |> List.tryPick isInLine

| SyntaxNode.SynModuleOrNamespace(SynModuleOrNamespace(longId = longId; xmlDoc = xmlDoc)) when
longIdentContainsPos longId pos && xmlDoc.IsEmpty
->
Some false

| SyntaxNode.SynModule(SynModuleDecl.NestedModule(
moduleInfo = SynComponentInfo(longId = longId; xmlDoc = xmlDoc))) when
longIdentContainsPos longId pos && xmlDoc.IsEmpty
->
Some false

| SyntaxNode.SynMemberDefn(SynMemberDefn.AutoProperty(ident = ident; xmlDoc = xmlDoc)) when
rangeContainsPos ident.idRange pos && xmlDoc.IsEmpty
->
Some true

| SyntaxNode.SynMemberDefn(SynMemberDefn.GetSetMember(
memberDefnForGet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId)))))
| SyntaxNode.SynMemberDefn(SynMemberDefn.GetSetMember(
memberDefnForSet = Some(SynBinding(xmlDoc = xmlDoc; headPat = SynPat.LongIdent(longDotId = longDotId))))) when
rangeContainsPos longDotId.Range pos && xmlDoc.IsEmpty
->
Some false

| _ -> None)

let trimmed = lineStr.TrimStart(' ')
let indentLength = lineStr.Length - trimmed.Length
Expand Down
Loading