Skip to content

Commit f414147

Browse files
committed
initial nested language support
1 parent c482f7e commit f414147

File tree

11 files changed

+463
-175
lines changed

11 files changed

+463
-175
lines changed

src/FsAutoComplete.Core/Commands.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ type NotificationEvent =
8383
| Canceled of errorMessage: string
8484
| FileParsed of string<LocalPath>
8585
| TestDetected of file: string<LocalPath> * tests: TestAdapter.TestAdapterEntry<range>[]
86+
| NestedLanguagesFound of
87+
file: string<LocalPath> *
88+
version: int *
89+
nestedLanguages: NestedLanguages.NestedLanguageDocument array
8690

8791
module Commands =
8892
open System.Collections.Concurrent

src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<TargetFrameworks Condition="'$(BuildNet7)' == 'true'">net6.0;net7.0</TargetFrameworks>
55
<TargetFrameworks Condition="'$(BuildNet8)' == 'true'">net6.0;net7.0;net8.0</TargetFrameworks>
66
<IsPackable>false</IsPackable>
7+
<LangVersion>preview</LangVersion>
78
</PropertyGroup>
89
<ItemGroup>
910
<ProjectReference Include="..\FsAutoComplete.Logging\FsAutoComplete.Logging.fsproj" />
@@ -58,6 +59,7 @@
5859
<Compile Include="SignatureHelp.fs" />
5960
<Compile Include="InlayHints.fs" />
6061
<Compile Include="SymbolLocation.fs" />
62+
<Compile Include="NestedLanguages.fs" />
6163
<Compile Include="Commands.fs" />
6264
</ItemGroup>
6365
<Import Project="..\..\.paket\Paket.Restore.targets" />
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
}

src/FsAutoComplete.Core/UntypedAstUtils.fs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@ module Syntax =
2626

2727
loop [] pats
2828

29+
[<AbstractClass>]
2930
type SyntaxCollectorBase() =
3031
abstract WalkSynModuleOrNamespace: SynModuleOrNamespace -> unit
3132
default _.WalkSynModuleOrNamespace _ = ()
3233
abstract WalkAttribute: SynAttribute -> unit
33-
default _.WalkAttribute _ = ()
34+
default _.WalkAttribute(_: SynAttribute) = ()
3435
abstract WalkSynModuleDecl: SynModuleDecl -> unit
3536
default _.WalkSynModuleDecl _ = ()
3637
abstract WalkExpr: SynExpr -> unit
@@ -59,8 +60,10 @@ module Syntax =
5960
default _.WalkClause _ = ()
6061
abstract WalkInterpolatedStringPart: SynInterpolatedStringPart -> unit
6162
default _.WalkInterpolatedStringPart _ = ()
63+
6264
abstract WalkMeasure: SynMeasure -> unit
63-
default _.WalkMeasure _ = ()
65+
default _.WalkMeasure(_: SynMeasure) = ()
66+
6467
abstract WalkComponentInfo: SynComponentInfo -> unit
6568
default _.WalkComponentInfo _ = ()
6669
abstract WalkTypeDefnSigRepr: SynTypeDefnSigRepr -> unit

src/FsAutoComplete.Core/UntypedAstUtils.fsi

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,65 @@ namespace FSharp.Compiler
33
module Syntax =
44
open FSharp.Compiler.Syntax
55

6+
[<AbstractClass>]
67
type SyntaxCollectorBase =
78
new: unit -> SyntaxCollectorBase
89
abstract WalkSynModuleOrNamespace: SynModuleOrNamespace -> unit
10+
default WalkSynModuleOrNamespace: SynModuleOrNamespace -> unit
911
abstract WalkAttribute: SynAttribute -> unit
12+
default WalkAttribute: SynAttribute -> unit
1013
abstract WalkSynModuleDecl: SynModuleDecl -> unit
14+
default WalkSynModuleDecl: SynModuleDecl -> unit
1115
abstract WalkExpr: SynExpr -> unit
16+
default WalkExpr: SynExpr -> unit
1217
abstract WalkTypar: SynTypar -> unit
18+
default WalkTypar: SynTypar -> unit
1319
abstract WalkTyparDecl: SynTyparDecl -> unit
20+
default WalkTyparDecl: SynTyparDecl -> unit
1421
abstract WalkTypeConstraint: SynTypeConstraint -> unit
22+
default WalkTypeConstraint: SynTypeConstraint -> unit
1523
abstract WalkType: SynType -> unit
24+
default WalkType: SynType -> unit
1625
abstract WalkMemberSig: SynMemberSig -> unit
26+
default WalkMemberSig: SynMemberSig -> unit
1727
abstract WalkPat: SynPat -> unit
28+
default WalkPat: SynPat -> unit
1829
abstract WalkValTyparDecls: SynValTyparDecls -> unit
30+
default WalkValTyparDecls: SynValTyparDecls -> unit
1931
abstract WalkBinding: SynBinding -> unit
32+
default WalkBinding: SynBinding -> unit
2033
abstract WalkSimplePat: SynSimplePat -> unit
34+
default WalkSimplePat: SynSimplePat -> unit
2135
abstract WalkInterfaceImpl: SynInterfaceImpl -> unit
36+
default WalkInterfaceImpl: SynInterfaceImpl -> unit
2237
abstract WalkClause: SynMatchClause -> unit
38+
default WalkClause: SynMatchClause -> unit
2339
abstract WalkInterpolatedStringPart: SynInterpolatedStringPart -> unit
40+
default WalkInterpolatedStringPart: SynInterpolatedStringPart -> unit
2441
abstract WalkMeasure: SynMeasure -> unit
42+
default WalkMeasure: SynMeasure -> unit
2543
abstract WalkComponentInfo: SynComponentInfo -> unit
44+
default WalkComponentInfo: SynComponentInfo -> unit
2645
abstract WalkTypeDefnSigRepr: SynTypeDefnSigRepr -> unit
46+
default WalkTypeDefnSigRepr: SynTypeDefnSigRepr -> unit
2747
abstract WalkUnionCaseType: SynUnionCaseKind -> unit
48+
default WalkUnionCaseType: SynUnionCaseKind -> unit
2849
abstract WalkEnumCase: SynEnumCase -> unit
50+
default WalkEnumCase: SynEnumCase -> unit
2951
abstract WalkField: SynField -> unit
52+
default WalkField: SynField -> unit
3053
abstract WalkTypeDefnSimple: SynTypeDefnSimpleRepr -> unit
54+
default WalkTypeDefnSimple: SynTypeDefnSimpleRepr -> unit
3155
abstract WalkValSig: SynValSig -> unit
56+
default WalkValSig: SynValSig -> unit
3257
abstract WalkMember: SynMemberDefn -> unit
58+
default WalkMember: SynMemberDefn -> unit
3359
abstract WalkUnionCase: SynUnionCase -> unit
60+
default WalkUnionCase: SynUnionCase -> unit
3461
abstract WalkTypeDefnRepr: SynTypeDefnRepr -> unit
62+
default WalkTypeDefnRepr: SynTypeDefnRepr -> unit
3563
abstract WalkTypeDefn: SynTypeDefn -> unit
64+
default WalkTypeDefn: SynTypeDefn -> unit
3665

3766
val walkAst: walker: SyntaxCollectorBase -> input: ParsedInput -> unit
3867

src/FsAutoComplete/LspServers/AdaptiveServerState.fs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,19 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac
594594
{ File = Path.LocalPathToUri file
595595
Tests = tests |> Array.map map }
596596
|> lspClient.NotifyTestDetected
597+
| NotificationEvent.NestedLanguagesFound(file, version, nestedLanguages) ->
598+
let uri = Path.LocalPathToUri file
599+
600+
do!
601+
lspClient.NotifyNestedLanguages(
602+
{ TextDocument = { Version = version; Uri = uri }
603+
NestedLanguages =
604+
nestedLanguages
605+
|> Array.map (fun n ->
606+
{ Language = n.Language
607+
Ranges = n.Ranges |> Array.map fcsRangeToLsp }) }
608+
)
609+
597610
with ex ->
598611
logger.error (
599612
Log.setMessage "Exception while handling command event {evt}: {ex}"

src/FsAutoComplete/LspServers/FSharpLspClient.fs

Lines changed: 12 additions & 1 deletion
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

@@ -62,6 +69,10 @@ type FSharpLspClient(sendServerNotification: ClientNotificationSender, sendServe
6269
member __.NotifyTestDetected(p: TestDetectedNotification) =
6370
sendServerNotification "fsharp/testDetected" (box p) |> Async.Ignore
6471

72+
member _.NotifyNestedLanguages(p: TextDocumentNestedLanguages) =
73+
sendServerNotification "fsharp/textDocument/nestedLanguages" (box p)
74+
|> Async.Ignore
75+
6576
member x.CodeLensRefresh() =
6677
match x.ClientCapabilities with
6778
| Some { Workspace = Some { CodeLens = Some { RefreshSupport = Some true } } } ->

0 commit comments

Comments
 (0)