@@ -14,23 +14,96 @@ let logger = LogProvider.getLoggerByName "NestedLanguages"
14
14
type private StringParameter =
15
15
{ methodIdent: LongIdent
16
16
parameterRange: Range
17
- rangesToRemove: Range []
17
+ rangesToRemove: Range array
18
18
parameterPosition: int }
19
19
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 )) |])
26
82
27
83
let private (| Ident | _ |) ( e : SynExpr ) =
28
84
match e with
29
85
| SynExpr.Ident( ident) -> Some([ ident ])
30
86
| SynExpr.LongIdent( longDotId = SynLongIdent( id = ident)) -> Some ident
31
87
| _ -> None
32
88
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 =
34
107
match e with
35
108
// lines inside a binding
36
109
// let doThing () =
@@ -39,34 +112,46 @@ let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option<St
39
112
// "<div>" |> c.M
40
113
// $"<div>{1 + 1}" |> c.M
41
114
| 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)
51
132
52
133
// method call with string parameter - c.M("<div>")
53
134
| 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), _)))
55
136
// 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), _)) ->
57
138
Some(
58
139
[| { methodIdent = ident
59
140
parameterRange = range
60
- rangesToRemove = [||]
141
+ rangesToRemove = removeStringTokensFromStringRange kind range
61
142
parameterPosition = 0 } |]
62
143
)
63
144
// method call with interpolated string parameter - c.M $"<div>{1 + 1}"
64
145
| SynExpr.App(
65
146
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)))
67
149
// 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)
70
155
71
156
Some(
72
157
[| { methodIdent = ident
@@ -117,28 +202,37 @@ let private (|IsStringSyntax|_|) (a: FSharpAttribute) =
117
202
| _ -> None
118
203
| _ -> None
119
204
120
- type NestedLanguageDocument = { Language: string ; Ranges: Range [] }
205
+ type NestedLanguageDocument =
206
+ { Language: string
207
+ Ranges: Range array }
121
208
122
- let rangeMinusRanges ( totalRange : Range ) ( rangesToRemove : Range [] ) : Range [] =
209
+ let rangeMinusRanges ( totalRange : Range ) ( rangesToRemove : Range array ) : Range array =
123
210
match rangesToRemove with
124
211
| [||] -> [| totalRange |]
125
212
| _ ->
126
213
let mutable returnVal = ResizeArray()
127
214
let mutable currentStart = totalRange.Start
128
215
129
216
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)
132
227
133
- returnVal.Add( Range.mkRange totalRange.FileName currentStart totalRange.End)
134
228
returnVal.ToArray()
135
229
136
230
let private parametersThatAreStringSyntax
137
231
(
138
- parameters : StringParameter [] ,
232
+ parameters : StringParameter array ,
139
233
checkResults : FSharpCheckFileResults ,
140
234
text : VolatileFile
141
- ) : Async < NestedLanguageDocument []> =
235
+ ) : NestedLanguageDocument array Async =
142
236
async {
143
237
let returnVal = ResizeArray()
144
238
@@ -194,90 +288,12 @@ let private parametersThatAreStringSyntax
194
288
return returnVal.ToArray()
195
289
}
196
290
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
-
274
291
/// to find all of the nested language highlights, we're going to do the following:
275
292
/// * find all of the interpolated strings or string literals in the file that are in parameter-application positions
276
293
/// * get the method calls happening at those positions to check if that method has the StringSyntaxAttribute
277
294
/// * 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 =
295
+ let findNestedLanguages ( tyRes : ParseAndCheckResults , text : VolatileFile ) : NestedLanguageDocument array Async =
279
296
async {
280
- // get all string constants
281
297
let potentialParameters = findParametersForParseTree tyRes.GetAST
282
298
283
299
logger.info (
@@ -291,9 +307,8 @@ let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : Nest
291
307
$" Potential parameter: {p.parameterRange.ToString():range} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
292
308
)
293
309
294
- //let! singleStringParameters = hasSingleStringParameter (potentialParameters, tyRes.GetCheckResults, text)
295
310
let! actualStringSyntaxParameters = parametersThatAreStringSyntax ( potentialParameters, tyRes.GetCheckResults, text)
296
- //let actualStringSyntaxParameters = Array.append singleStringParameters stringSyntaxParameters
311
+
297
312
logger.info (
298
313
Log.setMessageI
299
314
$" Found {actualStringSyntaxParameters.Length:stringParams} actual parameters in {text.FileName:filename}@{text.Version:version}"
0 commit comments