@@ -14,23 +14,96 @@ let logger = LogProvider.getLoggerByName "NestedLanguages"
1414type private StringParameter =
1515 { methodIdent: LongIdent
1616 parameterRange: Range
17- rangesToRemove: Range []
17+ rangesToRemove: Range array
1818 parameterPosition: int }
1919
20- let discoverRangesToRemoveForInterpolatedString ( list : SynInterpolatedStringPart list ) =
21- list
22- |> List.choose ( function
23- | SynInterpolatedStringPart.FillExpr( fillExpr = e) -> Some e.Range
24- | _ -> None)
25- |> List.toArray
20+
21+ /// for virtual documents based on interpolated strings we need to remove two kinds of trivia from the overall string portions.
22+ /// * for interpolation expressions we need to remove the entire range of the expression - this will be invisible to the virtual document since it is F# code.
23+ /// * for string literals, we need to remove the prefix/suffix tokens (quotes, interpolation brackets, format specifiers, etc) so that the only content visible
24+ /// to the virtual document is the actual string content.
25+ ///
26+ /// FEATURE GAP: we don't know in the AST the locations of the string trivia, so we can't support format specifiers or variable-length
27+ /// interpolation start/end tokens.
28+ let private discoverRangesToRemoveForInterpolatedString
29+ ( stringKind : SynStringKind )
30+ ( parts : SynInterpolatedStringPart [])
31+ =
32+ parts
33+ |> Array.indexed
34+ |> Array.collect ( fun ( index , part ) ->
35+ match part with
36+ | SynInterpolatedStringPart.FillExpr( fillExpr = e) -> [| e.Range |]
37+ // for the first part we have whatever 'leading' element on the left and a trailing interpolation piece (which can include a format specifier) on the right
38+ | SynInterpolatedStringPart.String( range = range) when index = 0 ->
39+ [|
40+ // leading tokens adjustment
41+ // GAP: we don't know how many interpolation $ or " there are, so we are guessing
42+ match stringKind with
43+ | SynStringKind.Regular ->
44+ // 'regular' means $" leading identifier
45+ range.WithEnd( range.Start.WithColumn( range.StartColumn + 2 ))
46+ | SynStringKind.TripleQuote ->
47+ // 'triple quote' means $""" leading identifier
48+ range.WithEnd( range.Start.WithColumn( range.StartColumn + 4 ))
49+ // there's no such thing as a verbatim interpolated string
50+ | SynStringKind.Verbatim -> ()
51+
52+ // trailing token adjustment- only an opening bracket {
53+ // GAP: this is the feature gap - we don't know about format specifiers
54+ range.WithStart( range.End.WithColumn( range.EndColumn - 1 ))
55+
56+ |]
57+ // for the last part we have a single-character interpolation bracket on the left and the 'trailing' string elements on the right
58+ | SynInterpolatedStringPart.String( range = range) when index = parts.Length - 1 ->
59+ [|
60+ // leading token adjustment - only a closing bracket }
61+ range.WithEnd( range.Start.WithColumn( range.StartColumn + 1 ))
62+
63+ // trailing tokens adjustment
64+ // GAP: we don't know how many """ to adjust for triple-quote interpolated string endings
65+ match stringKind with
66+ | SynStringKind.Regular ->
67+ // 'regular' means trailing identifier "
68+ range.WithStart( range.End.WithColumn( range.EndColumn - 1 ))
69+ | SynStringKind.TripleQuote ->
70+ // 'triple quote' means trailing identifier """
71+ range.WithStart( range.End.WithColumn( range.EndColumn - 3 ))
72+ // no such thing as verbatim interpolated strings
73+ | SynStringKind.Verbatim -> () |]
74+ // for all other parts we have a single-character interpolation bracket on the left and a trailing interpolation piece (which can include a format specifier) on the right
75+ | SynInterpolatedStringPart.String( range = range) ->
76+ [|
77+ // leading token adjustment - only a closing bracket }
78+ range.WithEnd( range.Start.WithColumn( range.StartColumn + 1 ))
79+ // trailing token adjustment- only an opening bracket {
80+ // GAP: this is the feature gap - we don't know about format specifiers here
81+ range.WithStart( range.End.WithColumn( range.EndColumn - 1 )) |])
2682
2783let private (| Ident | _ |) ( e : SynExpr ) =
2884 match e with
2985 | SynExpr.Ident( ident) -> Some([ ident ])
3086 | SynExpr.LongIdent( longDotId = SynLongIdent( id = ident)) -> Some ident
3187 | _ -> None
3288
33- let rec private (| IsApplicationWithStringParameters | _ |) ( e : SynExpr ) : option < StringParameter []> =
89+ /// in order for nested documents to be recognized as their document types, the string quotes (and other tokens) need to be removed
90+ /// from the actual string content.
91+ let private removeStringTokensFromStringRange ( kind : SynStringKind ) ( range : Range ) : Range array =
92+ match kind with
93+ | SynStringKind.Regular ->
94+ // we need to trim the double-quote off of the start and end
95+ [| Range.mkRange range.FileName range.Start ( range.Start.WithColumn( range.StartColumn + 1 ))
96+ Range.mkRange range.FileName ( range.End.WithColumn( range.EndColumn - 1 )) range.End |]
97+ | SynStringKind.Verbatim ->
98+ // we need to trim the @+double-quote off of the start and double-quote off the end
99+ [| Range.mkRange range.FileName range.Start ( range.Start.WithColumn( range.StartColumn + 2 ))
100+ Range.mkRange range.FileName ( range.End.WithColumn( range.EndColumn - 1 )) range.End |]
101+ | SynStringKind.TripleQuote ->
102+ // we need to trim the @+double-quote off of the start and double-quote off the end
103+ [| Range.mkRange range.FileName range.Start ( range.Start.WithColumn( range.StartColumn + 2 ))
104+ Range.mkRange range.FileName ( range.End.WithColumn( range.EndColumn - 1 )) range.End |]
105+
106+ let rec private (| IsApplicationWithStringParameters | _ |) ( e : SynExpr ) : StringParameter array option =
34107 match e with
35108 // lines inside a binding
36109 // let doThing () =
@@ -39,34 +112,46 @@ let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option<St
39112 // "<div>" |> c.M
40113 // $"<div>{1 + 1}" |> c.M
41114 | SynExpr.Sequential( expr1 = e1; expr2 = e2) ->
42- [| match e1 with
43- | IsApplicationWithStringParameters( stringParameter) -> yield ! stringParameter
44- | _ -> ()
45-
46- match e2 with
47- | IsApplicationWithStringParameters( stringParameter) -> yield ! stringParameter
48- | _ -> () |]
49- // TODO: check if the array would be empty and return none
50- |> Some
115+ let e1Parameters =
116+ match e1 with
117+ | IsApplicationWithStringParameters( stringParameter) when not ( Array.isEmpty stringParameter) ->
118+ ValueSome stringParameter
119+ | _ -> ValueNone
120+
121+ let e2Parameters =
122+ match e2 with
123+ | IsApplicationWithStringParameters( stringParameter) when not ( Array.isEmpty stringParameter) ->
124+ ValueSome stringParameter
125+ | _ -> ValueNone
126+
127+ match e1Parameters, e2Parameters with
128+ | ValueNone, ValueNone -> None
129+ | ValueSome e1Parameters, ValueNone -> Some e1Parameters
130+ | ValueNone, ValueSome e2Parameters -> Some e2Parameters
131+ | ValueSome e1Parameters, ValueSome e2Parameters -> Some( Array.append e1Parameters e2Parameters)
51132
52133 // method call with string parameter - c.M("<div>")
53134 | SynExpr.App(
54- funcExpr = Ident( ident); argExpr = SynExpr.Paren( expr = SynExpr.Const( SynConst.String(_ text, _ kind , range), _)))
135+ funcExpr = Ident( ident); argExpr = SynExpr.Paren( expr = SynExpr.Const( SynConst.String(_ text, kind , range), _)))
55136 // method call with string parameter - c.M "<div>"
56- | SynExpr.App( funcExpr = Ident( ident); argExpr = SynExpr.Const( SynConst.String(_ text, _ kind , range), _)) ->
137+ | SynExpr.App( funcExpr = Ident( ident); argExpr = SynExpr.Const( SynConst.String(_ text, kind , range), _)) ->
57138 Some(
58139 [| { methodIdent = ident
59140 parameterRange = range
60- rangesToRemove = [||]
141+ rangesToRemove = removeStringTokensFromStringRange kind range
61142 parameterPosition = 0 } |]
62143 )
63144 // method call with interpolated string parameter - c.M $"<div>{1 + 1}"
64145 | SynExpr.App(
65146 funcExpr = Ident( ident)
66- argExpr = SynExpr.Paren( expr = SynExpr.InterpolatedString( contents = parts; range = range)))
147+ argExpr = SynExpr.Paren(
148+ expr = SynExpr.InterpolatedString( contents = parts; synStringKind = stringKind; range = range)))
67149 // method call with interpolated string parameter - c.M($"<div>{1 + 1}")
68- | SynExpr.App( funcExpr = Ident( ident); argExpr = SynExpr.InterpolatedString( contents = parts; range = range)) ->
69- let rangesToRemove = discoverRangesToRemoveForInterpolatedString parts
150+ | SynExpr.App(
151+ funcExpr = Ident( ident)
152+ argExpr = SynExpr.InterpolatedString( contents = parts; synStringKind = stringKind; range = range)) ->
153+ let rangesToRemove =
154+ discoverRangesToRemoveForInterpolatedString stringKind ( Array.ofList parts)
70155
71156 Some(
72157 [| { methodIdent = ident
@@ -117,28 +202,34 @@ let private (|IsStringSyntax|_|) (a: FSharpAttribute) =
117202 | _ -> None
118203 | _ -> None
119204
120- type NestedLanguageDocument = { Language: string ; Ranges: Range [] }
205+ type NestedLanguageDocument =
206+ { Language: string
207+ Ranges: Range array }
121208
122- let rangeMinusRanges ( totalRange : Range ) ( rangesToRemove : Range [] ) : Range [] =
209+ let rangeMinusRanges ( totalRange : Range ) ( rangesToRemove : Range array ) : Range array =
123210 match rangesToRemove with
124211 | [||] -> [| totalRange |]
125212 | _ ->
126213 let mutable returnVal = ResizeArray()
127214 let mutable currentStart = totalRange.Start
128215
129216 for r in rangesToRemove do
130- returnVal.Add( Range.mkRange totalRange.FileName currentStart r.Start)
131- currentStart <- r.End
217+ if currentStart = r.Start then
218+ // no gaps, so just advance the current pointer
219+ currentStart <- r.End
220+ else
221+ returnVal.Add( Range.mkRange totalRange.FileName currentStart r.Start)
222+ currentStart <- r.End
223+
224+ // only need to add the final range if there is a gap between where we are and the end of the string
225+ if currentStart <> totalRange.End then
226+ returnVal.Add( Range.mkRange totalRange.FileName currentStart totalRange.End)
132227
133- returnVal.Add( Range.mkRange totalRange.FileName currentStart totalRange.End)
134228 returnVal.ToArray()
135229
136230let private parametersThatAreStringSyntax
137- (
138- parameters : StringParameter [],
139- checkResults : FSharpCheckFileResults ,
140- text : VolatileFile
141- ) : Async < NestedLanguageDocument []> =
231+ ( parameters : StringParameter array , checkResults : FSharpCheckFileResults , text : VolatileFile )
232+ : NestedLanguageDocument array Async =
142233 async {
143234 let returnVal = ResizeArray()
144235
@@ -194,90 +285,12 @@ let private parametersThatAreStringSyntax
194285 return returnVal.ToArray()
195286 }
196287
197- let private safeNestedLanguageNames =
198- System.Collections.Generic.HashSet(
199- [ " html" ; " svg" ; " css" ; " sql" ; " js" ; " python" ; " uri" ; " regex" ; " xml" ; " json" ],
200- System.StringComparer.OrdinalIgnoreCase
201- )
202-
203- let private hasSingleStringParameter
204- (
205- parameters : StringParameter [],
206- checkResults : FSharpCheckFileResults ,
207- text : VolatileFile
208- ) : Async < NestedLanguageDocument []> =
209- async {
210- let returnVal = ResizeArray()
211-
212- for p in parameters do
213- logger.info (
214- Log.setMessageI
215- $" Checking parameter: {p.parameterRange.ToString():range} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
216- )
217-
218- let lastPart = p.methodIdent[^ 0 ]
219- let endOfFinalTextToken = lastPart.idRange.End
220-
221- match text.Source.GetLine( endOfFinalTextToken) with
222- | None -> ()
223- | Some lineText ->
224-
225- match
226- checkResults.GetSymbolUseAtLocation(
227- endOfFinalTextToken.Line,
228- endOfFinalTextToken.Column + 1 ,
229- lineText,
230- p.methodIdent |> List.map ( fun x -> x.idText)
231- )
232- with
233- | None -> ()
234- | Some usage ->
235- logger.info (
236- Log.setMessageI
237- $" Found symbol use: {usage.Symbol.ToString():symbol} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
238- )
239-
240- let sym = usage.Symbol
241- // todo: keep MRU map of symbols to parameters and MRU of parameters to StringSyntax status
242-
243- match sym with
244- | : ? FSharpMemberOrFunctionOrValue as mfv ->
245- let languageName = sym.DisplayName // TODO: what about funky names?
246-
247- if safeNestedLanguageNames.Contains( languageName) then
248- let allParameters = mfv.CurriedParameterGroups |> Seq.collect id
249- let firstParameter = allParameters |> Seq.tryHead
250- let hasOthers = allParameters |> Seq.skip 1 |> Seq.isEmpty |> not
251-
252- match hasOthers, firstParameter with
253- | _, None -> ()
254- | true , _ -> ()
255- | false , Some fsharpP ->
256- logger.info (
257- Log.setMessageI
258- $" Found parameter: {fsharpP.ToString():symbol} with {fsharpP.Attributes.Count:attributeCount} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
259- )
260-
261- let baseType = fsharpP.Type.StripAbbreviations()
262-
263- if baseType.BasicQualifiedName = " System.String" then
264- returnVal.Add
265- { Language = languageName
266- Ranges = rangeMinusRanges p.parameterRange p.rangesToRemove }
267- else
268- ()
269- | _ -> ()
270-
271- return returnVal.ToArray()
272- }
273-
274288/// to find all of the nested language highlights, we're going to do the following:
275289/// * find all of the interpolated strings or string literals in the file that are in parameter-application positions
276290/// * get the method calls happening at those positions to check if that method has the StringSyntaxAttribute
277291/// * if so, return a) the language in the StringSyntaxAttribute, and b) the range of the interpolated string
278- let findNestedLanguages ( tyRes : ParseAndCheckResults , text : VolatileFile ) : NestedLanguageDocument [] Async =
292+ let findNestedLanguages ( tyRes : ParseAndCheckResults , text : VolatileFile ) : NestedLanguageDocument array Async =
279293 async {
280- // get all string constants
281294 let potentialParameters = findParametersForParseTree tyRes.GetAST
282295
283296 logger.info (
@@ -291,9 +304,8 @@ let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : Nest
291304 $" Potential parameter: {p.parameterRange.ToString():range} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
292305 )
293306
294- //let! singleStringParameters = hasSingleStringParameter (potentialParameters, tyRes.GetCheckResults, text)
295307 let! actualStringSyntaxParameters = parametersThatAreStringSyntax ( potentialParameters, tyRes.GetCheckResults, text)
296- //let actualStringSyntaxParameters = Array.append singleStringParameters stringSyntaxParameters
308+
297309 logger.info (
298310 Log.setMessageI
299311 $" Found {actualStringSyntaxParameters.Length:stringParams} actual parameters in {text.FileName:filename}@{text.Version:version}"
0 commit comments