Skip to content

Commit

Permalink
initial nested language support
Browse files Browse the repository at this point in the history
  • Loading branch information
baronfel committed Nov 25, 2023
1 parent c482f7e commit f414147
Show file tree
Hide file tree
Showing 11 changed files with 463 additions and 175 deletions.
4 changes: 4 additions & 0 deletions src/FsAutoComplete.Core/Commands.fs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ type NotificationEvent =
| Canceled of errorMessage: string
| FileParsed of string<LocalPath>
| TestDetected of file: string<LocalPath> * tests: TestAdapter.TestAdapterEntry<range>[]
| NestedLanguagesFound of
file: string<LocalPath> *
version: int *
nestedLanguages: NestedLanguages.NestedLanguageDocument array

module Commands =
open System.Collections.Concurrent
Expand Down
2 changes: 2 additions & 0 deletions src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFrameworks Condition="'$(BuildNet7)' == 'true'">net6.0;net7.0</TargetFrameworks>
<TargetFrameworks Condition="'$(BuildNet8)' == 'true'">net6.0;net7.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FsAutoComplete.Logging\FsAutoComplete.Logging.fsproj" />
Expand Down Expand Up @@ -58,6 +59,7 @@
<Compile Include="SignatureHelp.fs" />
<Compile Include="InlayHints.fs" />
<Compile Include="SymbolLocation.fs" />
<Compile Include="NestedLanguages.fs" />
<Compile Include="Commands.fs" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
Expand Down
190 changes: 190 additions & 0 deletions src/FsAutoComplete.Core/NestedLanguages.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
module FsAutoComplete.NestedLanguages

open FsToolkit.ErrorHandling
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Symbols

#nowarn "57" // from-end slicing

type private StringParameter =
{ methodIdent: LongIdent
parameterRange: Range
rangesToRemove: Range[]
parameterPosition: int }

let discoverRangesToRemoveForInterpolatedString (list: SynInterpolatedStringPart list) =
list
|> List.choose (function
| SynInterpolatedStringPart.FillExpr(fillExpr = e) -> Some e.Range
| _ -> None)
|> List.toArray

let private (|Ident|_|) (e: SynExpr) =
match e with
| SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) -> Some ident
| _ -> None

let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option<StringParameter[]> =
match e with
// lines inside a binding
// let doThing () =
// c.M("<div>")
// c.M($"<div>{1 + 1}")
// "<div>" |> c.M
// $"<div>{1 + 1}" |> c.M
| SynExpr.Sequential(expr1 = e1; expr2 = e2) ->
[| match e1 with
| IsApplicationWithStringParameters(stringParameter) -> yield! stringParameter
| _ -> ()

match e2 with
| IsApplicationWithStringParameters(stringParameter) -> yield! stringParameter
| _ -> () |]
// TODO: check if the array would be empty and return none
|> Some

// method call with string parameter - c.M("<div>")
| SynExpr.App(
funcExpr = Ident(ident); argExpr = SynExpr.Paren(expr = SynExpr.Const(SynConst.String(_text, _kind, range), _)))
// method call with string parameter - c.M "<div>"
| SynExpr.App(funcExpr = Ident(ident); argExpr = SynExpr.Const(SynConst.String(_text, _kind, range), _)) ->
Some(
[| { methodIdent = ident
parameterRange = range
rangesToRemove = [||]
parameterPosition = 0 } |]
)
// method call with interpolated string parameter - c.M $"<div>{1 + 1}"
| SynExpr.App(
funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = ident))
argExpr = SynExpr.Paren(expr = SynExpr.InterpolatedString(contents = parts; range = range)))
// method call with interpolated string parameter - c.M($"<div>{1 + 1}")
| SynExpr.App(
funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = ident))
argExpr = SynExpr.InterpolatedString(contents = parts; range = range)) ->
let rangesToRemove = discoverRangesToRemoveForInterpolatedString parts

Some(
[| { methodIdent = ident
parameterRange = range
rangesToRemove = rangesToRemove
parameterPosition = 0 } |]
)
// piped method call with string parameter - "<div>" |> c.M
// piped method call with interpolated parameter - $"<div>{1 + 1}" |> c.M
// method call with multiple string or interpolated string parameters (this also covers the case when not all parameters of the member are strings)
// c.M("<div>", true) and/or c.M(true, "<div>")
// piped method call with multiple string or interpolated string parameters (this also covers the case when not all parameters of the member are strings)
// let binding that is a string value that has the stringsyntax attribute on it - [<StringSyntax("html")>] let html = "<div />"
// all of the above but with literals
| _ -> None

/// <summary></summary>
type private StringParameterFinder() =
inherit SyntaxCollectorBase()

let languages = ResizeArray<StringParameter>()

override _.WalkBinding(SynBinding(expr = expr)) =
match expr with
| IsApplicationWithStringParameters(stringParameters) -> languages.AddRange stringParameters
| _ -> ()

override _.WalkSynModuleDecl(decl) =
match decl with
| SynModuleDecl.Expr(expr = IsApplicationWithStringParameters(stringParameters)) ->
languages.AddRange stringParameters
| _ -> ()

member _.NestedLanguages = languages.ToArray()


let private findParametersForParseTree (p: ParsedInput) =
let walker = StringParameterFinder()
walkAst walker p
walker.NestedLanguages

let private (|IsStringSyntax|_|) (a: FSharpAttribute) =
match a.AttributeType.FullName with
| "System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" ->
match a.ConstructorArguments |> Seq.tryHead with
| Some(_ty, languageValue) -> Some(languageValue :?> string)
| _ -> None
| _ -> None

type NestedLanguageDocument = { Language: string; Ranges: Range[] }

let rangeMinusRanges (totalRange: Range) (rangesToRemove: Range[]) : Range[] =
match rangesToRemove with
| [||] -> [| totalRange |]
| _ ->
let mutable returnVal = ResizeArray()
let mutable currentStart = totalRange.Start

for r in rangesToRemove do
returnVal.Add(Range.mkRange totalRange.FileName currentStart r.Start)
currentStart <- r.End

returnVal.Add(Range.mkRange totalRange.FileName currentStart totalRange.End)
returnVal.ToArray()

let private parametersThatAreStringSyntax
(
parameters: StringParameter[],
checkResults: FSharpCheckFileResults,
text: IFSACSourceText
) : Async<NestedLanguageDocument[]> =
async {
let returnVal = ResizeArray()

for p in parameters do
let precedingParts, lastPart = p.methodIdent.[0..^1], p.methodIdent[^0]
let endOfFinalTextToken = lastPart.idRange.End

match text.GetLine(endOfFinalTextToken) with
| None -> ()
| Some lineText ->

match
checkResults.GetSymbolUseAtLocation(
endOfFinalTextToken.Line,
endOfFinalTextToken.Column,
lineText,
precedingParts |> List.map (fun i -> i.idText)
)
with
| None -> ()
| Some usage ->

let sym = usage.Symbol
// todo: keep MRU map of symbols to parameters and MRU of parameters to stringsyntax status

match sym with
| :? FSharpMemberOrFunctionOrValue as mfv ->
let allParameters = mfv.CurriedParameterGroups |> Seq.collect id |> Seq.toArray
let fsharpP = allParameters |> Seq.item p.parameterPosition

match fsharpP.Attributes |> Seq.tryPick (|IsStringSyntax|_|) with
| Some language ->
returnVal.Add
{ Language = language
Ranges = rangeMinusRanges p.parameterRange p.rangesToRemove }
| None -> ()
| _ -> ()

return returnVal.ToArray()
}

/// to find all of the nested language highlights, we're going to do the following:
/// * find all of the interpolated strings or string literals in the file that are in parameter-application positions
/// * get the method calls happening at those positions to check if that method has the StringSyntaxAttribute
/// * if so, return a) the language in the StringSyntaxAttribute, and b) the range of the interpolated string
let findNestedLanguages (tyRes: ParseAndCheckResults, text: IFSACSourceText) : NestedLanguageDocument[] Async =
async {
// get all string constants
let potentialParameters = findParametersForParseTree tyRes.GetAST
let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text)
return actualStringSyntaxParameters
}
7 changes: 5 additions & 2 deletions src/FsAutoComplete.Core/UntypedAstUtils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ module Syntax =

loop [] pats

[<AbstractClass>]
type SyntaxCollectorBase() =
abstract WalkSynModuleOrNamespace: SynModuleOrNamespace -> unit
default _.WalkSynModuleOrNamespace _ = ()
abstract WalkAttribute: SynAttribute -> unit
default _.WalkAttribute _ = ()
default _.WalkAttribute(_: SynAttribute) = ()
abstract WalkSynModuleDecl: SynModuleDecl -> unit
default _.WalkSynModuleDecl _ = ()
abstract WalkExpr: SynExpr -> unit
Expand Down Expand Up @@ -59,8 +60,10 @@ module Syntax =
default _.WalkClause _ = ()
abstract WalkInterpolatedStringPart: SynInterpolatedStringPart -> unit
default _.WalkInterpolatedStringPart _ = ()

abstract WalkMeasure: SynMeasure -> unit
default _.WalkMeasure _ = ()
default _.WalkMeasure(_: SynMeasure) = ()

abstract WalkComponentInfo: SynComponentInfo -> unit
default _.WalkComponentInfo _ = ()
abstract WalkTypeDefnSigRepr: SynTypeDefnSigRepr -> unit
Expand Down
29 changes: 29 additions & 0 deletions src/FsAutoComplete.Core/UntypedAstUtils.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,65 @@ namespace FSharp.Compiler
module Syntax =
open FSharp.Compiler.Syntax

[<AbstractClass>]
type SyntaxCollectorBase =
new: unit -> SyntaxCollectorBase
abstract WalkSynModuleOrNamespace: SynModuleOrNamespace -> unit
default WalkSynModuleOrNamespace: SynModuleOrNamespace -> unit
abstract WalkAttribute: SynAttribute -> unit
default WalkAttribute: SynAttribute -> unit
abstract WalkSynModuleDecl: SynModuleDecl -> unit
default WalkSynModuleDecl: SynModuleDecl -> unit
abstract WalkExpr: SynExpr -> unit
default WalkExpr: SynExpr -> unit
abstract WalkTypar: SynTypar -> unit
default WalkTypar: SynTypar -> unit
abstract WalkTyparDecl: SynTyparDecl -> unit
default WalkTyparDecl: SynTyparDecl -> unit
abstract WalkTypeConstraint: SynTypeConstraint -> unit
default WalkTypeConstraint: SynTypeConstraint -> unit
abstract WalkType: SynType -> unit
default WalkType: SynType -> unit
abstract WalkMemberSig: SynMemberSig -> unit
default WalkMemberSig: SynMemberSig -> unit
abstract WalkPat: SynPat -> unit
default WalkPat: SynPat -> unit
abstract WalkValTyparDecls: SynValTyparDecls -> unit
default WalkValTyparDecls: SynValTyparDecls -> unit
abstract WalkBinding: SynBinding -> unit
default WalkBinding: SynBinding -> unit
abstract WalkSimplePat: SynSimplePat -> unit
default WalkSimplePat: SynSimplePat -> unit
abstract WalkInterfaceImpl: SynInterfaceImpl -> unit
default WalkInterfaceImpl: SynInterfaceImpl -> unit
abstract WalkClause: SynMatchClause -> unit
default WalkClause: SynMatchClause -> unit
abstract WalkInterpolatedStringPart: SynInterpolatedStringPart -> unit
default WalkInterpolatedStringPart: SynInterpolatedStringPart -> unit
abstract WalkMeasure: SynMeasure -> unit
default WalkMeasure: SynMeasure -> unit
abstract WalkComponentInfo: SynComponentInfo -> unit
default WalkComponentInfo: SynComponentInfo -> unit
abstract WalkTypeDefnSigRepr: SynTypeDefnSigRepr -> unit
default WalkTypeDefnSigRepr: SynTypeDefnSigRepr -> unit
abstract WalkUnionCaseType: SynUnionCaseKind -> unit
default WalkUnionCaseType: SynUnionCaseKind -> unit
abstract WalkEnumCase: SynEnumCase -> unit
default WalkEnumCase: SynEnumCase -> unit
abstract WalkField: SynField -> unit
default WalkField: SynField -> unit
abstract WalkTypeDefnSimple: SynTypeDefnSimpleRepr -> unit
default WalkTypeDefnSimple: SynTypeDefnSimpleRepr -> unit
abstract WalkValSig: SynValSig -> unit
default WalkValSig: SynValSig -> unit
abstract WalkMember: SynMemberDefn -> unit
default WalkMember: SynMemberDefn -> unit
abstract WalkUnionCase: SynUnionCase -> unit
default WalkUnionCase: SynUnionCase -> unit
abstract WalkTypeDefnRepr: SynTypeDefnRepr -> unit
default WalkTypeDefnRepr: SynTypeDefnRepr -> unit
abstract WalkTypeDefn: SynTypeDefn -> unit
default WalkTypeDefn: SynTypeDefn -> unit

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

Expand Down
13 changes: 13 additions & 0 deletions src/FsAutoComplete/LspServers/AdaptiveServerState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,19 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac
{ File = Path.LocalPathToUri file
Tests = tests |> Array.map map }
|> lspClient.NotifyTestDetected
| NotificationEvent.NestedLanguagesFound(file, version, nestedLanguages) ->
let uri = Path.LocalPathToUri file

do!
lspClient.NotifyNestedLanguages(
{ TextDocument = { Version = version; Uri = uri }
NestedLanguages =
nestedLanguages
|> Array.map (fun n ->
{ Language = n.Language
Ranges = n.Ranges |> Array.map fcsRangeToLsp }) }
)

with ex ->
logger.error (
Log.setMessage "Exception while handling command event {evt}: {ex}"
Expand Down
13 changes: 12 additions & 1 deletion src/FsAutoComplete/LspServers/FSharpLspClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ namespace FsAutoComplete.Lsp


open Ionide.LanguageServerProtocol
open Ionide.LanguageServerProtocol.Types.LspResult
open Ionide.LanguageServerProtocol.Server
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.LspHelpers
Expand All @@ -12,6 +11,14 @@ open FsAutoComplete.Utils
open System.Threading
open IcedTasks

type NestedLanguage =
{ Language: string
Ranges: Types.Range[] }

type TextDocumentNestedLanguages =
{ TextDocument: VersionedTextDocumentIdentifier
NestedLanguages: NestedLanguage[] }


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

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

member _.NotifyNestedLanguages(p: TextDocumentNestedLanguages) =
sendServerNotification "fsharp/textDocument/nestedLanguages" (box p)
|> Async.Ignore

member x.CodeLensRefresh() =
match x.ClientCapabilities with
| Some { Workspace = Some { CodeLens = Some { RefreshSupport = Some true } } } ->
Expand Down
Loading

0 comments on commit f414147

Please sign in to comment.