|
| 1 | +module FsAutoComplete.NestedLanguages |
| 2 | + |
| 3 | +open FsToolkit.ErrorHandling |
| 4 | +open FSharp.Compiler.Syntax |
| 5 | +open FSharp.Compiler.Text |
| 6 | +open FSharp.Compiler.CodeAnalysis |
| 7 | +open FSharp.Compiler.Symbols |
| 8 | + |
| 9 | +#nowarn "57" // from-end slicing |
| 10 | + |
| 11 | +type private StringParameter = |
| 12 | + { methodIdent: LongIdent |
| 13 | + parameterRange: Range |
| 14 | + rangesToRemove: Range[] |
| 15 | + parameterPosition: int } |
| 16 | + |
| 17 | +let discoverRangesToRemoveForInterpolatedString (list: SynInterpolatedStringPart list) = |
| 18 | + list |
| 19 | + |> List.choose (function |
| 20 | + | SynInterpolatedStringPart.FillExpr(fillExpr = e) -> Some e.Range |
| 21 | + | _ -> None) |
| 22 | + |> List.toArray |
| 23 | + |
| 24 | +let private (|Ident|_|) (e: SynExpr) = |
| 25 | + match e with |
| 26 | + | SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) -> Some ident |
| 27 | + | _ -> None |
| 28 | + |
| 29 | +let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option<StringParameter[]> = |
| 30 | + match e with |
| 31 | + // lines inside a binding |
| 32 | + // let doThing () = |
| 33 | + // c.M("<div>") |
| 34 | + // c.M($"<div>{1 + 1}") |
| 35 | + // "<div>" |> c.M |
| 36 | + // $"<div>{1 + 1}" |> c.M |
| 37 | + | SynExpr.Sequential(expr1 = e1; expr2 = e2) -> |
| 38 | + [| match e1 with |
| 39 | + | IsApplicationWithStringParameters(stringParameter) -> yield! stringParameter |
| 40 | + | _ -> () |
| 41 | + |
| 42 | + match e2 with |
| 43 | + | IsApplicationWithStringParameters(stringParameter) -> yield! stringParameter |
| 44 | + | _ -> () |] |
| 45 | + // TODO: check if the array would be empty and return none |
| 46 | + |> Some |
| 47 | + |
| 48 | + // method call with string parameter - c.M("<div>") |
| 49 | + | SynExpr.App( |
| 50 | + funcExpr = Ident(ident); argExpr = SynExpr.Paren(expr = SynExpr.Const(SynConst.String(_text, _kind, range), _))) |
| 51 | + // method call with string parameter - c.M "<div>" |
| 52 | + | SynExpr.App(funcExpr = Ident(ident); argExpr = SynExpr.Const(SynConst.String(_text, _kind, range), _)) -> |
| 53 | + Some( |
| 54 | + [| { methodIdent = ident |
| 55 | + parameterRange = range |
| 56 | + rangesToRemove = [||] |
| 57 | + parameterPosition = 0 } |] |
| 58 | + ) |
| 59 | + // method call with interpolated string parameter - c.M $"<div>{1 + 1}" |
| 60 | + | SynExpr.App( |
| 61 | + funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) |
| 62 | + argExpr = SynExpr.Paren(expr = SynExpr.InterpolatedString(contents = parts; range = range))) |
| 63 | + // method call with interpolated string parameter - c.M($"<div>{1 + 1}") |
| 64 | + | SynExpr.App( |
| 65 | + funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) |
| 66 | + argExpr = SynExpr.InterpolatedString(contents = parts; range = range)) -> |
| 67 | + let rangesToRemove = discoverRangesToRemoveForInterpolatedString parts |
| 68 | + |
| 69 | + Some( |
| 70 | + [| { methodIdent = ident |
| 71 | + parameterRange = range |
| 72 | + rangesToRemove = rangesToRemove |
| 73 | + parameterPosition = 0 } |] |
| 74 | + ) |
| 75 | + // piped method call with string parameter - "<div>" |> c.M |
| 76 | + // piped method call with interpolated parameter - $"<div>{1 + 1}" |> c.M |
| 77 | + // method call with multiple string or interpolated string parameters (this also covers the case when not all parameters of the member are strings) |
| 78 | + // c.M("<div>", true) and/or c.M(true, "<div>") |
| 79 | + // piped method call with multiple string or interpolated string parameters (this also covers the case when not all parameters of the member are strings) |
| 80 | + // let binding that is a string value that has the stringsyntax attribute on it - [<StringSyntax("html")>] let html = "<div />" |
| 81 | + // all of the above but with literals |
| 82 | + | _ -> None |
| 83 | + |
| 84 | +/// <summary></summary> |
| 85 | +type private StringParameterFinder() = |
| 86 | + inherit SyntaxCollectorBase() |
| 87 | + |
| 88 | + let languages = ResizeArray<StringParameter>() |
| 89 | + |
| 90 | + override _.WalkBinding(SynBinding(expr = expr)) = |
| 91 | + match expr with |
| 92 | + | IsApplicationWithStringParameters(stringParameters) -> languages.AddRange stringParameters |
| 93 | + | _ -> () |
| 94 | + |
| 95 | + override _.WalkSynModuleDecl(decl) = |
| 96 | + match decl with |
| 97 | + | SynModuleDecl.Expr(expr = IsApplicationWithStringParameters(stringParameters)) -> |
| 98 | + languages.AddRange stringParameters |
| 99 | + | _ -> () |
| 100 | + |
| 101 | + member _.NestedLanguages = languages.ToArray() |
| 102 | + |
| 103 | + |
| 104 | +let private findParametersForParseTree (p: ParsedInput) = |
| 105 | + let walker = StringParameterFinder() |
| 106 | + walkAst walker p |
| 107 | + walker.NestedLanguages |
| 108 | + |
| 109 | +let private (|IsStringSyntax|_|) (a: FSharpAttribute) = |
| 110 | + match a.AttributeType.FullName with |
| 111 | + | "System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" -> |
| 112 | + match a.ConstructorArguments |> Seq.tryHead with |
| 113 | + | Some(_ty, languageValue) -> Some(languageValue :?> string) |
| 114 | + | _ -> None |
| 115 | + | _ -> None |
| 116 | + |
| 117 | +type NestedLanguageDocument = { Language: string; Ranges: Range[] } |
| 118 | + |
| 119 | +let rangeMinusRanges (totalRange: Range) (rangesToRemove: Range[]) : Range[] = |
| 120 | + match rangesToRemove with |
| 121 | + | [||] -> [| totalRange |] |
| 122 | + | _ -> |
| 123 | + let mutable returnVal = ResizeArray() |
| 124 | + let mutable currentStart = totalRange.Start |
| 125 | + |
| 126 | + for r in rangesToRemove do |
| 127 | + returnVal.Add(Range.mkRange totalRange.FileName currentStart r.Start) |
| 128 | + currentStart <- r.End |
| 129 | + |
| 130 | + returnVal.Add(Range.mkRange totalRange.FileName currentStart totalRange.End) |
| 131 | + returnVal.ToArray() |
| 132 | + |
| 133 | +let private parametersThatAreStringSyntax |
| 134 | + ( |
| 135 | + parameters: StringParameter[], |
| 136 | + checkResults: FSharpCheckFileResults, |
| 137 | + text: IFSACSourceText |
| 138 | + ) : Async<NestedLanguageDocument[]> = |
| 139 | + async { |
| 140 | + let returnVal = ResizeArray() |
| 141 | + |
| 142 | + for p in parameters do |
| 143 | + let precedingParts, lastPart = p.methodIdent.[0..^1], p.methodIdent[^0] |
| 144 | + let endOfFinalTextToken = lastPart.idRange.End |
| 145 | + |
| 146 | + match text.GetLine(endOfFinalTextToken) with |
| 147 | + | None -> () |
| 148 | + | Some lineText -> |
| 149 | + |
| 150 | + match |
| 151 | + checkResults.GetSymbolUseAtLocation( |
| 152 | + endOfFinalTextToken.Line, |
| 153 | + endOfFinalTextToken.Column, |
| 154 | + lineText, |
| 155 | + precedingParts |> List.map (fun i -> i.idText) |
| 156 | + ) |
| 157 | + with |
| 158 | + | None -> () |
| 159 | + | Some usage -> |
| 160 | + |
| 161 | + let sym = usage.Symbol |
| 162 | + // todo: keep MRU map of symbols to parameters and MRU of parameters to stringsyntax status |
| 163 | + |
| 164 | + match sym with |
| 165 | + | :? FSharpMemberOrFunctionOrValue as mfv -> |
| 166 | + let allParameters = mfv.CurriedParameterGroups |> Seq.collect id |> Seq.toArray |
| 167 | + let fsharpP = allParameters |> Seq.item p.parameterPosition |
| 168 | + |
| 169 | + match fsharpP.Attributes |> Seq.tryPick (|IsStringSyntax|_|) with |
| 170 | + | Some language -> |
| 171 | + returnVal.Add |
| 172 | + { Language = language |
| 173 | + Ranges = rangeMinusRanges p.parameterRange p.rangesToRemove } |
| 174 | + | None -> () |
| 175 | + | _ -> () |
| 176 | + |
| 177 | + return returnVal.ToArray() |
| 178 | + } |
| 179 | + |
| 180 | +/// to find all of the nested language highlights, we're going to do the following: |
| 181 | +/// * find all of the interpolated strings or string literals in the file that are in parameter-application positions |
| 182 | +/// * get the method calls happening at those positions to check if that method has the StringSyntaxAttribute |
| 183 | +/// * if so, return a) the language in the StringSyntaxAttribute, and b) the range of the interpolated string |
| 184 | +let findNestedLanguages (tyRes: ParseAndCheckResults, text: IFSACSourceText) : NestedLanguageDocument[] Async = |
| 185 | + async { |
| 186 | + // get all string constants |
| 187 | + let potentialParameters = findParametersForParseTree tyRes.GetAST |
| 188 | + let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text) |
| 189 | + return actualStringSyntaxParameters |
| 190 | + } |
0 commit comments