Skip to content

Commit

Permalink
Find node in selection using tree format.
Browse files Browse the repository at this point in the history
  • Loading branch information
nojaf committed May 30, 2022
1 parent c1136fd commit d6e5ebb
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 51 deletions.
2 changes: 1 addition & 1 deletion src/Fantomas.Core/AstTransformer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ module private Ast =
| SynExpr.Sequential (_, _, expr1, expr2, _) ->
visit expr2 (fun node1 ->
visit expr1 (fun node2 ->
mkNodeWithChildren SynExpr_Sequential synExpr.Range (sortChildren [| node1; node2 |])
mkSynExprNode SynExpr_Sequential synExpr synExpr.Range (sortChildren [| node1; node2 |])
|> finalContinuation))
| SynExpr.SequentialOrImplicitYield (_, expr1, expr2, ifNotStmt, range) ->
let continuations: ((TriviaNodeAssigner -> TriviaNodeAssigner) -> TriviaNodeAssigner) list =
Expand Down
2 changes: 2 additions & 0 deletions src/Fantomas.Core/CodeFormatter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type CodeFormatter =
CodeFormatterImpl.getSourceText source
|> CodeFormatterImpl.formatDocument config isSignature

// TODO: should this return the range of the actual formatted node
// The selection might have been larger than the actual formatted node
static member FormatSelectionAsync(isSignature, source, selection, config) =
CodeFormatterImpl.getSourceText source
|> Selection.formatSelection config isSignature selection
Expand Down
21 changes: 16 additions & 5 deletions src/Fantomas.Core/RangeHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ open FSharp.Compiler.Text
module RangeHelpers =

/// Checks if Range B is fully contained by Range A
let ``range contains`` (a: Range) (b: Range) =
(a.Start.Line, a.Start.Column)
<= (b.Start.Line, b.Start.Column)
&& (a.End.Line, a.End.Column)
>= (b.End.Line, b.End.Column)
let ``range contains`` (a: Range) (b: Range) = Range.rangeContainsRange a b

// check if b is after a
let ``range after`` (a: Range) (b: Range) =
Expand Down Expand Up @@ -52,6 +48,21 @@ module RangeHelpers =
|> List.reduce Range.unionRanges
|> Some

/// Calculate an artificial surface area based on the range.
let surfaceArea (maxLineLength: int) (range: range) : int =
// Calculate an artificial surface of positions they range consume.
// Take the max_line_length as size for a blank line
// This isn't totally accurate, but will do the trick.
let linesInBetween =
match [ range.StartLine + 1 .. range.EndLine - 1 ] with
| []
| [ _ ] -> 0
| lines -> lines.Length * maxLineLength

(maxLineLength - range.StartColumn)
+ linesInBetween
+ range.EndColumn

module RangePatterns =
let (|StartEndRange|) (size: int) (range: range) =
let o, c = RangeHelpers.mkStartEndRange size range
Expand Down
75 changes: 44 additions & 31 deletions src/Fantomas.Core/Selection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@ open Fantomas.Core.AstExtensions
open Fantomas.Core.TriviaTypes
open Fantomas.Core.AstTransformer

// TODO: check Option.isSome tna.FSharpASTNode
let rec private findNodeWhereSelectionFitsIn (root: TriviaNodeAssigner) (selection: range) : TriviaNodeAssigner option =
let doesSelectionFitInNode = RangeHelpers.``range contains`` root.Range selection

if not doesSelectionFitInNode then
None
else
// The more specific the node fits the selection, the better
let findDeeperNode =
root.Children
|> Array.choose (fun childNode -> findNodeWhereSelectionFitsIn childNode selection)

match Array.tryHead findDeeperNode with
| Some betterChild -> Some betterChild
| None -> Some root

let private findNode (maxLineLength: int) (selection: range) (node: TriviaNodeAssigner) : TriviaNodeAssigner option =
let selectionSurface = RangeHelpers.surfaceArea maxLineLength selection

// Some parent nodes should be filtered out
let rec selectNode (node: TriviaNodeAssigner) : TriviaNodeAssigner array =
match node.Type with
| SynExpr_Sequential -> Array.collect selectNode node.Children
| _ -> [| node |]

[| yield! selectNode node
yield! Array.collect selectNode node.Children |]
|> Array.filter (fun a -> Option.isSome a.FSharpASTNode)
|> Array.sortBy (fun n ->
// Find the node that matches the selection as close as possible
let nodeSurface = RangeHelpers.surfaceArea maxLineLength n.Range
System.Math.Abs(selectionSurface - nodeSurface))
|> Array.tryHead

let private mkAnonSynModuleOrNamespace decl =
SynModuleOrNamespace(
[],
Expand Down Expand Up @@ -70,47 +104,26 @@ let formatSelection
let isValid = Validation.noWarningOrErrorDiagnostics baseDiagnostics

if not isValid then
failwith "not valid"
return None
else
let triviaNodes =
let triviaNode =
match baseUntypedTree with
| ImplFile (ParsedImplFileInput (hds, mns, _, _)) -> astToNode baseUntypedTree.FullRange hds mns
| SigFile (ParsedSigFileInput (_, mns, _, _)) -> sigAstToNode baseUntypedTree.FullRange mns
//|> List.sortBy (fun n -> n.Range.Start.Line, n.Range.Start.Column)

let selection =
Range.mkRange triviaNode.Range.FileName selection.Start selection.End

let treeWithSelection =
// triviaNodes
[]
|> List.filter (fun (tna: TriviaNodeAssigner) ->
Option.isSome tna.FSharpASTNode
&& RangeHelpers.``range contains`` selection tna.Range)
|> List.sortByDescending (fun tna ->
if tna.Range.StartLine = tna.Range.EndLine then
tna.Range.EndColumn - tna.Range.StartColumn
else
// Calculate an artificial surface of positions they range consume.
// Take the max_line_length as size for a blank line
// This isn't totally accurate, but will do the trick.
// The larger the surface, the closer to the selection the node is.
let linesInBetween =
match [ tna.Range.StartLine + 1 .. tna.Range.EndLine - 1 ] with
| []
| [ _ ] -> 0
| lines -> lines.Length * config.MaxLineLength

(config.MaxLineLength - tna.Range.StartColumn)
+ linesInBetween
+ tna.Range.EndColumn)
#if DEBUG
|> fun tap ->
printfn "sorted: %A" tap
tap
#endif
|> List.tryHead
findNodeWhereSelectionFitsIn triviaNode selection
|> Option.bind (findNode config.MaxLineLength selection)
|> Option.bind (fun tna -> Option.map (mkTreeWithSingleNode baseUntypedTree) tna.FSharpASTNode)

match treeWithSelection with
| None -> return None
| None ->
failwithf "no node found, %A %A" selection baseUntypedTree
return None
| Some tree ->
let maxLineLength = config.MaxLineLength - selection.StartColumn

Expand Down
35 changes: 21 additions & 14 deletions src/Fantomas.Core/Trivia.fs
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,10 @@ let internal collectTriviaFromCodeComments (source: ISourceText) (codeComments:
let internal collectTriviaFromBlankLines
(config: FormatConfig)
(source: ISourceText)
(triviaNodes: TriviaNodeAssigner list)
(triviaNode: TriviaNodeAssigner)
(codeComments: CommentTrivia list)
: Trivia list =
let fileIndex = triviaNodes.Head.Range.FileIndex
let fileIndex = triviaNode.Range.FileIndex

let captureLinesIfMultiline (r: range) =
if r.StartLine = r.EndLine then
Expand All @@ -257,13 +257,24 @@ let internal collectTriviaFromBlankLines
[ r.StartLine .. r.EndLine ]

let multilineStringsLines =
triviaNodes
|> List.collect (fun tn ->
match tn.Type with
| SynConst_String
| SynConst_Bytes
| SynInterpolatedStringPart_String -> captureLinesIfMultiline tn.Range
| _ -> [])
let rec visit (node: TriviaNodeAssigner) (finalContinuation: int list -> int list) =
let continuations: ((int list -> int list) -> int list) list =
Array.toList node.Children |> List.map visit

let currentLines =
match node.Type with
| SynConst_String
| SynConst_Bytes
| SynInterpolatedStringPart_String -> captureLinesIfMultiline node.Range
| _ -> []

let finalContinuation (lines: int list list) : int list =
List.collect id (currentLines :: lines)
|> finalContinuation

Continuation.sequence continuations finalContinuation

visit triviaNode id

let blockCommentLines =
codeComments
Expand Down Expand Up @@ -314,14 +325,10 @@ let collectTrivia (config: FormatConfig) (source: ISourceText) (ast: ParsedInput
| ParsedInput.SigFile (ParsedSigFileInput (_, mns, directives, codeComments)) ->
sigAstToNode ast.FullRange mns, directives, codeComments

// let triviaNodes =
// triviaNodesFromAST
// |> List.sortBy (fun n -> n.Range.Start.Line, n.Range.Start.Column)

let trivia =
[ yield! collectTriviaFromDirectives source directives
yield! collectTriviaFromCodeComments source codeComments
yield! collectTriviaFromBlankLines config source [] (* triviaNodes *) codeComments ]
yield! collectTriviaFromBlankLines config source triviaNodesFromAST codeComments ]
|> List.sortBy (fun n -> n.Range.Start.Line, n.Range.Start.Column)

// TODO: do we still need this?
Expand Down

0 comments on commit d6e5ebb

Please sign in to comment.