|
| 1 | +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. |
| 2 | + |
| 3 | +module internal Microsoft.VisualStudio.FSharp.Editor.TupleConversion |
| 4 | + |
| 5 | +open System |
| 6 | + |
| 7 | +open Microsoft.CodeAnalysis.Text |
| 8 | + |
| 9 | +open FSharp.Compiler.Syntax |
| 10 | +open FSharp.Compiler.Text |
| 11 | + |
| 12 | +let spanOf (sourceText: SourceText) (m: range) = |
| 13 | + RoslynHelpers.FSharpRangeToTextSpan(sourceText, m) |
| 14 | + |
| 15 | +let isSame (node: 'T) (other: 'T) = obj.ReferenceEquals(node, other) |
| 16 | + |
| 17 | +let containsPos (m: range) (position: pos) = |
| 18 | + Position.posGeq position m.Start && Position.posGeq m.End position |
| 19 | + |
| 20 | +let rec stripParenTypes (ty: SynType) = |
| 21 | + match ty with |
| 22 | + | SynType.Paren(innerType = inner) -> stripParenTypes inner |
| 23 | + | _ -> ty |
| 24 | + |
| 25 | +/// `struct` and the blanks after it, at the start of a struct tuple's range. |
| 26 | +let private structKeyword (sourceText: SourceText) (m: range) = |
| 27 | + let start = (spanOf sourceText m).Start |
| 28 | + let mutable finish = start + "struct".Length |
| 29 | + |
| 30 | + while finish < sourceText.Length && Char.IsWhiteSpace sourceText[finish] do |
| 31 | + finish <- finish + 1 |
| 32 | + |
| 33 | + TextSpan.FromBounds(start, finish) |
| 34 | + |
| 35 | +/// The position of the `(` that, with its `)`, encloses only the span and blanks. |
| 36 | +let private tryEnclosingParen (sourceText: SourceText) (span: TextSpan) = |
| 37 | + let mutable before = span.Start - 1 |
| 38 | + |
| 39 | + while before >= 0 && Char.IsWhiteSpace sourceText[before] do |
| 40 | + before <- before - 1 |
| 41 | + |
| 42 | + let mutable after = span.End |
| 43 | + |
| 44 | + while after < sourceText.Length && Char.IsWhiteSpace sourceText[after] do |
| 45 | + after <- after + 1 |
| 46 | + |
| 47 | + if |
| 48 | + before >= 0 |
| 49 | + && after < sourceText.Length |
| 50 | + && sourceText[before] = '(' |
| 51 | + && sourceText[after] = ')' |
| 52 | + then |
| 53 | + ValueSome before |
| 54 | + else |
| 55 | + ValueNone |
| 56 | + |
| 57 | +/// Whether the text between start and finish is a whole generic argument: `<` or `,` before it, `>` or `,` after. |
| 58 | +let private isGenericArgument (sourceText: SourceText) (start: int) (finish: int) = |
| 59 | + let mutable before = start - 1 |
| 60 | + |
| 61 | + while before >= 0 && Char.IsWhiteSpace sourceText[before] do |
| 62 | + before <- before - 1 |
| 63 | + |
| 64 | + let mutable after = finish |
| 65 | + |
| 66 | + while after < sourceText.Length && Char.IsWhiteSpace sourceText[after] do |
| 67 | + after <- after + 1 |
| 68 | + |
| 69 | + before >= 0 |
| 70 | + && after < sourceText.Length |
| 71 | + && (sourceText[before] = '<' || sourceText[before] = ',') |
| 72 | + && (sourceText[after] = '>' || sourceText[after] = ',') |
| 73 | + |
| 74 | +/// Changes giving a tuple type the target kind; a whole annotation also loses the parentheses `struct` needed. |
| 75 | +let typeChanges (sourceText: SourceText) (toStruct: bool) (isWholeAnnotation: bool) (tupleType: SynType) = |
| 76 | + match tupleType with |
| 77 | + | SynType.Tuple(isStruct = isStruct; range = m) when isStruct <> toStruct -> |
| 78 | + let span = spanOf sourceText m |
| 79 | + |
| 80 | + if toStruct then |
| 81 | + match tryEnclosingParen sourceText span with |
| 82 | + | ValueSome openParen -> [ TextChange(TextSpan(openParen, 0), "struct ") ] |
| 83 | + | ValueNone -> |
| 84 | + [ |
| 85 | + TextChange(TextSpan(span.Start, 0), "struct (") |
| 86 | + TextChange(TextSpan(span.End, 0), ")") |
| 87 | + ] |
| 88 | + else |
| 89 | + let keyword = structKeyword sourceText m |
| 90 | + |
| 91 | + if isWholeAnnotation || isGenericArgument sourceText keyword.Start span.End then |
| 92 | + [ |
| 93 | + TextChange(TextSpan(keyword.Start, keyword.Length + 1), "") |
| 94 | + TextChange(TextSpan(span.End - 1, 1), "") |
| 95 | + ] |
| 96 | + else |
| 97 | + [ TextChange(keyword, "") ] |
| 98 | + | _ -> [] |
| 99 | + |
| 100 | +/// Whether the tuple is the argument list of a method, constructor or union case call rather than a tuple value. |
| 101 | +let isArgumentList (tuple: SynExpr) (path: SyntaxVisitorPath) = |
| 102 | + match path with |
| 103 | + | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner) as paren) :: SyntaxNode.SynExpr(SynExpr.App(flag = ExprAtomicFlag.Atomic; argExpr = arg) | SynExpr.New( |
| 104 | + expr = arg)) :: _ -> isSame inner tuple && isSame arg paren |
| 105 | + | _ -> false |
| 106 | + |
| 107 | +/// Changes giving a tuple expression the target kind; ValueNone when it is not a tuple or cannot change in place. |
| 108 | +let tryExprChanges (sourceText: SourceText) (toStruct: bool) (tuple: SynExpr) (path: SyntaxVisitorPath) = |
| 109 | + match tuple with |
| 110 | + | SynExpr.Tuple(isStruct = isStruct) when isStruct = toStruct -> ValueSome [] |
| 111 | + | SynExpr.Tuple(range = m) when not toStruct -> ValueSome [ TextChange(structKeyword sourceText m, "") ] |
| 112 | + | SynExpr.Tuple(range = m) -> |
| 113 | + match path with |
| 114 | + | SyntaxNode.SynExpr(SynExpr.Paren(expr = inner; range = parenRange)) :: _ when isSame inner tuple -> |
| 115 | + ValueSome [ TextChange(TextSpan((spanOf sourceText parenRange).Start, 0), "struct ") ] |
| 116 | + | _ when m.StartLine = m.EndLine -> |
| 117 | + let span = spanOf sourceText m |
| 118 | + |
| 119 | + ValueSome |
| 120 | + [ |
| 121 | + TextChange(TextSpan(span.Start, 0), "struct (") |
| 122 | + TextChange(TextSpan(span.End, 0), ")") |
| 123 | + ] |
| 124 | + | _ -> ValueNone |
| 125 | + | _ -> ValueNone |
| 126 | + |
| 127 | +/// Changes giving a tuple pattern the target kind; ValueNone when it is not a tuple or cannot change in place. |
| 128 | +let tryPatChanges (sourceText: SourceText) (toStruct: bool) (tuple: SynPat) (path: SyntaxVisitorPath) = |
| 129 | + match tuple with |
| 130 | + | SynPat.Tuple(isStruct = isStruct) when isStruct = toStruct -> ValueSome [] |
| 131 | + | SynPat.Tuple(range = m) when not toStruct -> ValueSome [ TextChange(structKeyword sourceText m, "") ] |
| 132 | + | SynPat.Tuple(range = m) -> |
| 133 | + match path with |
| 134 | + | SyntaxNode.SynPat(SynPat.Paren(pat = inner; range = parenRange)) :: _ when isSame inner tuple -> |
| 135 | + ValueSome [ TextChange(TextSpan((spanOf sourceText parenRange).Start, 0), "struct ") ] |
| 136 | + | _ when m.StartLine = m.EndLine -> |
| 137 | + let span = spanOf sourceText m |
| 138 | + |
| 139 | + ValueSome |
| 140 | + [ |
| 141 | + TextChange(TextSpan(span.Start, 0), "struct (") |
| 142 | + TextChange(TextSpan(span.End, 0), ")") |
| 143 | + ] |
| 144 | + | _ -> ValueNone |
| 145 | + | _ -> ValueNone |
| 146 | + |
| 147 | +/// The innermost tuple type within the type that contains the position. |
| 148 | +let rec tryTupleTypeAt (position: pos) (ty: SynType) = |
| 149 | + if not (containsPos ty.Range position) then |
| 150 | + ValueNone |
| 151 | + else |
| 152 | + let inner = |
| 153 | + match ty with |
| 154 | + | SynType.Paren(innerType = inner) |
| 155 | + | SynType.Array(elementType = inner) |
| 156 | + | SynType.WithGlobalConstraints(typeName = inner) -> tryTupleTypeAt position inner |
| 157 | + | SynType.App(typeName = typeName; typeArgs = typeArgs) |
| 158 | + | SynType.LongIdentApp(typeName = typeName; typeArgs = typeArgs) -> |
| 159 | + typeName :: typeArgs |> Seq.tryPickV (tryTupleTypeAt position) |
| 160 | + | SynType.Fun(argType = argType; returnType = returnType) -> [ argType; returnType ] |> Seq.tryPickV (tryTupleTypeAt position) |
| 161 | + | SynType.Tuple(path = segments) -> |
| 162 | + segments |
| 163 | + |> Seq.tryPickV (function |
| 164 | + | SynTupleTypeSegment.Type element -> tryTupleTypeAt position element |
| 165 | + | _ -> ValueNone) |
| 166 | + | _ -> ValueNone |
| 167 | + |
| 168 | + match inner, ty with |
| 169 | + | ValueSome _, _ -> inner |
| 170 | + | ValueNone, SynType.Tuple _ -> ValueSome ty |
| 171 | + | ValueNone, _ -> ValueNone |
| 172 | + |
| 173 | +/// What a tuple type under the caret annotates. |
| 174 | +[<RequireQualifiedAccess; NoComparison; NoEquality>] |
| 175 | +type Annotated = |
| 176 | + | Pattern of pat: SynPat * path: SyntaxVisitorPath |
| 177 | + | Expression of expr: SynExpr * path: SyntaxVisitorPath |
| 178 | + | Return of binding: SynBinding |
| 179 | + | Field of field: SynField |
| 180 | + |
| 181 | +[<RequireQualifiedAccess; NoComparison; NoEquality>] |
| 182 | +type CaretNode = |
| 183 | + | Expr of tuple: SynExpr * path: SyntaxVisitorPath |
| 184 | + | Pat of tuple: SynPat * path: SyntaxVisitorPath |
| 185 | + | Type of tuple: SynType * annotated: Annotated * isWholeAnnotation: bool |
| 186 | + |
| 187 | +let isStructNode (node: CaretNode) = |
| 188 | + match node with |
| 189 | + | CaretNode.Expr(tuple = SynExpr.Tuple(isStruct = isStruct)) |
| 190 | + | CaretNode.Pat(tuple = SynPat.Tuple(isStruct = isStruct)) |
| 191 | + | CaretNode.Type(tuple = SynType.Tuple(isStruct = isStruct)) -> isStruct |
| 192 | + | _ -> false |
| 193 | + |
| 194 | +/// The innermost tuple expression, pattern or annotated tuple type under the caret. |
| 195 | +let tryCaretNode (caret: pos) (parseTree: ParsedInput) = |
| 196 | + let annotationAt (annotation: SynType) (annotated: Annotated) = |
| 197 | + tryTupleTypeAt caret annotation |
| 198 | + |> ValueOption.map (fun tuple -> CaretNode.Type(tuple, annotated, isSame (stripParenTypes annotation) tuple)) |
| 199 | + |
| 200 | + (ValueNone, parseTree) |
| 201 | + ||> ParsedInput.fold (fun found path node -> |
| 202 | + match node with |
| 203 | + | SyntaxNode.SynExpr(SynExpr.Tuple(range = m) as tuple) when containsPos m caret && not (isArgumentList tuple path) -> |
| 204 | + ValueSome(CaretNode.Expr(tuple, path)) |
| 205 | + | SyntaxNode.SynPat(SynPat.Tuple(range = m) as tuple) when containsPos m caret -> ValueSome(CaretNode.Pat(tuple, path)) |
| 206 | + | SyntaxNode.SynPat(SynPat.Typed(targetType = annotation) as pat) when containsPos annotation.Range caret -> |
| 207 | + annotationAt annotation (Annotated.Pattern(pat, path)) |
| 208 | + |> ValueOption.orElse found |
| 209 | + | SyntaxNode.SynExpr(SynExpr.Typed(targetType = annotation) as expr) when containsPos annotation.Range caret -> |
| 210 | + annotationAt annotation (Annotated.Expression(expr, path)) |
| 211 | + |> ValueOption.orElse found |
| 212 | + | SyntaxNode.SynBinding(SynBinding(returnInfo = Some(SynBindingReturnInfo(typeName = annotation))) as binding) when |
| 213 | + containsPos annotation.Range caret |
| 214 | + -> |
| 215 | + annotationAt annotation (Annotated.Return binding) |> ValueOption.orElse found |
| 216 | + | SyntaxNode.SynTypeDefn(SynTypeDefn( |
| 217 | + typeRepr = SynTypeDefnRepr.Simple(SynTypeDefnSimpleRepr.Record(recordFieldsAndSpreads = fields), _))) -> |
| 218 | + fields |
| 219 | + |> Seq.tryPickV (function |
| 220 | + | SynFieldOrSpread.Field(SynField(fieldType = annotation) as field) when containsPos annotation.Range caret -> |
| 221 | + annotationAt annotation (Annotated.Field field) |
| 222 | + | _ -> ValueNone) |
| 223 | + |> ValueOption.orElse found |
| 224 | + | _ -> found) |
0 commit comments