Skip to content

Commit 18e4dda

Browse files
committed
a lot more testing and fine tuning based on Ionide integration work
1 parent b13a6c7 commit 18e4dda

File tree

2 files changed

+259
-146
lines changed

2 files changed

+259
-146
lines changed

src/FsAutoComplete.Core/NestedLanguages.fs

Lines changed: 126 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,96 @@ let logger = LogProvider.getLoggerByName "NestedLanguages"
1414
type 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

2783
let 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,37 @@ 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

136230
let private parametersThatAreStringSyntax
137231
(
138-
parameters: StringParameter[],
232+
parameters: StringParameter array,
139233
checkResults: FSharpCheckFileResults,
140234
text: VolatileFile
141-
) : Async<NestedLanguageDocument[]> =
235+
) : NestedLanguageDocument array Async =
142236
async {
143237
let returnVal = ResizeArray()
144238

@@ -194,90 +288,12 @@ let private parametersThatAreStringSyntax
194288
return returnVal.ToArray()
195289
}
196290

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-
274291
/// to find all of the nested language highlights, we're going to do the following:
275292
/// * find all of the interpolated strings or string literals in the file that are in parameter-application positions
276293
/// * get the method calls happening at those positions to check if that method has the StringSyntaxAttribute
277294
/// * 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 =
279296
async {
280-
// get all string constants
281297
let potentialParameters = findParametersForParseTree tyRes.GetAST
282298

283299
logger.info (
@@ -291,9 +307,8 @@ let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : Nest
291307
$"Potential parameter: {p.parameterRange.ToString():range} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
292308
)
293309

294-
//let! singleStringParameters = hasSingleStringParameter (potentialParameters, tyRes.GetCheckResults, text)
295310
let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text)
296-
//let actualStringSyntaxParameters = Array.append singleStringParameters stringSyntaxParameters
311+
297312
logger.info (
298313
Log.setMessageI
299314
$"Found {actualStringSyntaxParameters.Length:stringParams} actual parameters in {text.FileName:filename}@{text.Version:version}"

0 commit comments

Comments
 (0)