Skip to content

Commit ddd5195

Browse files
committed
implement basic nested language support and add first end to end test
this is blocked by dotnet/fsharp#15925 because FCS APIs don't provide us with attribute lists
1 parent 257a9ce commit ddd5195

File tree

10 files changed

+419
-227
lines changed

10 files changed

+419
-227
lines changed

src/FsAutoComplete.Core/Commands.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ type NotificationEvent =
9292
| NestedLanguagesFound of
9393
file: string<LocalPath> *
9494
version: int *
95-
nestedLanguages: {| language: string; range: Range |}[]
95+
nestedLanguages: NestedLanguages.NestedLanguageDocument array
9696

9797
module Commands =
9898
open System.Collections.Concurrent

src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<TargetFrameworks>net6.0</TargetFrameworks>
44
<TargetFrameworks Condition="'$(BuildNet7)' == 'true'">net6.0;net7.0</TargetFrameworks>
55
<IsPackable>false</IsPackable>
6+
<LangVersion>preview</LangVersion>
67
</PropertyGroup>
78
<ItemGroup>
89
<ProjectReference Include="..\FsAutoComplete.Logging\FsAutoComplete.Logging.fsproj" />

src/FsAutoComplete.Core/NestedLanguages.fs

Lines changed: 175 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,189 @@ module FsAutoComplete.NestedLanguages
22

33
open FsToolkit.ErrorHandling
44
open FSharp.Compiler.Syntax
5-
open FSharp.Compiler.Syntax.SyntaxTraversal
65
open FSharp.Compiler.Text
6+
open FSharp.Compiler.CodeAnalysis
7+
open FSharp.Compiler.Symbols
8+
9+
#nowarn "57" // from-end slicing
710

811
type private StringParameter =
912
{ methodIdent: LongIdent
10-
parameterRange: Range }
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
1183

84+
/// <summary></summary>
1285
type private StringParameterFinder() =
13-
inherit SyntaxVisitorBase<StringParameter[]>()
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+
}
14179

15180
/// to find all of the nested language highlights, we're going to do the following:
16181
/// * find all of the interpolated strings or string literals in the file that are in parameter-application positions
17182
/// * get the method calls happening at those positions to check if that method has the StringSyntaxAttribute
18183
/// * if so, return a) the language in the StringSyntaxAttribute, and b) the range of the interpolated string
19-
let findNestedLanguages (tyRes: ParseAndCheckResults) = async { return [||] }
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+
}

src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ type AdaptiveFSharpLspServer
346346
use progress = new ServerProgressReport(lspClient)
347347
do! progress.Begin($"Checking simplifing of names {fileName}...", message = filePathUntag)
348348

349-
let! nestedLanguages = NestedLanguages.findNestedLanguages tyRes
349+
let! nestedLanguages = NestedLanguages.findNestedLanguages (tyRes, source)
350350
let! ct = Async.CancellationToken
351351
notifications.Trigger(NotificationEvent.NestedLanguagesFound(filePath, version, nestedLanguages), ct)
352352
with e ->
@@ -371,7 +371,9 @@ type AdaptiveFSharpLspServer
371371
config.SimplifyNameAnalyzer
372372
&& isNotExcluded config.SimplifyNameAnalyzerExclusions
373373
then
374-
checkSimplifiedNames ]
374+
checkSimplifiedNames
375+
// todo: add config flag for nested languages
376+
findNestedLanguages ]
375377

376378
async {
377379
do! analyzers |> Async.parallel75 |> Async.Ignore<unit[]>
@@ -631,14 +633,14 @@ type AdaptiveFSharpLspServer
631633
| NotificationEvent.NestedLanguagesFound(file, version, nestedLanguages) ->
632634
do!
633635
lspClient.NotifyNestedLanguages(
634-
{| nestedLanguages =
636+
{ NestedLanguages =
635637
nestedLanguages
636638
|> Array.map (fun l ->
637-
{| language = l.language
638-
range = fcsRangeToLsp l.range |})
639-
textDocument =
639+
{ Language = l.Language
640+
Ranges = l.Ranges |> Array.map fcsRangeToLsp })
641+
TextDocument =
640642
{ Uri = Path.LocalPathToUri file
641-
Version = version } |}
643+
Version = version } }
642644
)
643645
with ex ->
644646
logger.error (

src/FsAutoComplete/LspServers/FSharpLspClient.fs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ namespace FsAutoComplete.Lsp
22

33

44
open Ionide.LanguageServerProtocol
5-
open Ionide.LanguageServerProtocol.Types.LspResult
65
open Ionide.LanguageServerProtocol.Server
76
open Ionide.LanguageServerProtocol.Types
87
open FsAutoComplete.LspHelpers
@@ -12,6 +11,14 @@ open FsAutoComplete.Utils
1211
open System.Threading
1312
open IcedTasks
1413

14+
type NestedLanguage =
15+
{ Language: string
16+
Ranges: Types.Range[] }
17+
18+
type TextDocumentNestedLanguages =
19+
{ TextDocument: VersionedTextDocumentIdentifier
20+
NestedLanguages: NestedLanguage[] }
21+
1522

1623
type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServerRequest: ClientRequestSender) =
1724

@@ -72,13 +79,7 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe
7279
member __.NotifyTestDetected(p: TestDetectedNotification) =
7380
sendServerNotification "fsharp/testDetected" (box p) |> Async.Ignore
7481

75-
member _.NotifyNestedLanguages
76-
(p:
77-
{| textDocument: VersionedTextDocumentIdentifier
78-
nestedLanguages:
79-
{| language: string
80-
range: Ionide.LanguageServerProtocol.Types.Range |}[] |})
81-
=
82+
member _.NotifyNestedLanguages(p: TextDocumentNestedLanguages) =
8283
sendServerNotification "fsharp/textDocument/nestedLanguages" (box p)
8384
|> Async.Ignore
8485

src/FsAutoComplete/LspServers/FsAutoComplete.Lsp.fs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -386,14 +386,14 @@ type FSharpLspServer(state: State, lspClient: FSharpLspClient, sourceTextFactory
386386

387387
| NotificationEvent.NestedLanguagesFound(file, version, nestedLanguages) ->
388388
lspClient.NotifyNestedLanguages(
389-
{| nestedLanguages =
389+
{ NestedLanguages =
390390
nestedLanguages
391391
|> Array.map (fun l ->
392-
{| language = l.language
393-
range = fcsRangeToLsp l.range |})
394-
textDocument =
392+
{ Language = l.Language
393+
Ranges = l.Ranges |> Array.map fcsRangeToLsp })
394+
TextDocument =
395395
{ Uri = Path.LocalPathToUri file
396-
Version = version } |}
396+
Version = version } }
397397
)
398398
|> Async.Start
399399
with ex ->
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module FsAutoComplete.Tests.NestedLanguageTests
2+
3+
open Expecto
4+
open Utils.ServerTests
5+
open Helpers
6+
open Utils.Server
7+
open System
8+
open Ionide.LanguageServerProtocol.Types
9+
10+
type Document with
11+
12+
member x.NestedLanguages =
13+
x.Server.Events
14+
|> Document.typedEvents<FsAutoComplete.Lsp.TextDocumentNestedLanguages> ("fsharp/textDocument/nestedLanguages")
15+
|> Observable.filter (fun n -> n.TextDocument = x.VersionedTextDocumentIdentifier)
16+
17+
let hasLanguages name source expectedLanguages server =
18+
testAsync name {
19+
let! (doc, _) = server |> Server.createUntitledDocument source
20+
let! nestedLanguages = doc.NestedLanguages |> Async.AwaitObservable
21+
22+
let mappedExpectedLanguages: FsAutoComplete.Lsp.NestedLanguage array =
23+
expectedLanguages
24+
|> Array.map (fun (l, rs) ->
25+
{ Language = l
26+
Ranges =
27+
rs
28+
|> Array.map (fun ((sl, sc), (el, ec)) ->
29+
{ Start = { Line = sl; Character = sc }
30+
End = { Line = el; Character = ec } }) })
31+
32+
Expect.equal nestedLanguages.NestedLanguages mappedExpectedLanguages "languages"
33+
}
34+
35+
let tests state =
36+
testList
37+
"nested languages"
38+
[ serverTestList "class member" state defaultConfigDto None (fun server ->
39+
[ hasLanguages
40+
"with single string parameter"
41+
"""
42+
let b = System.UriBuilder("https://google.com")
43+
"""
44+
[| ("uri", [| (1, 38), (1, 58) |]) |]
45+
server ]) ]

0 commit comments

Comments
 (0)