{1 + 1}" |> c.M
+ | SynExpr.Sequential(expr1 = e1; expr2 = e2) ->
+ let e1Parameters =
+ match e1 with
+ | IsApplicationWithStringParameters(stringParameter) when not (Array.isEmpty stringParameter) ->
+ ValueSome stringParameter
+ | _ -> ValueNone
+
+ let e2Parameters =
+ match e2 with
+ | IsApplicationWithStringParameters(stringParameter) when not (Array.isEmpty stringParameter) ->
+ ValueSome stringParameter
+ | _ -> ValueNone
+
+ match e1Parameters, e2Parameters with
+ | ValueNone, ValueNone -> None
+ | ValueSome e1Parameters, ValueNone -> Some e1Parameters
+ | ValueNone, ValueSome e2Parameters -> Some e2Parameters
+ | ValueSome e1Parameters, ValueSome e2Parameters -> Some(Array.append e1Parameters e2Parameters)
+
+ // method call with string parameter - c.M("
")
+ | SynExpr.App(
+ funcExpr = Ident(ident); argExpr = SynExpr.Paren(expr = SynExpr.Const(SynConst.String(_text, kind, range), _)))
+ // method call with string parameter - c.M "
"
+ | SynExpr.App(funcExpr = Ident(ident); argExpr = SynExpr.Const(SynConst.String(_text, kind, range), _)) ->
+ Some(
+ [| { methodIdent = ident
+ parameterRange = range
+ rangesToRemove = removeStringTokensFromStringRange kind range
+ parameterPosition = 0 } |]
+ )
+ // method call with interpolated string parameter - c.M $"
{1 + 1}"
+ | SynExpr.App(
+ funcExpr = Ident(ident)
+ argExpr = SynExpr.Paren(
+ expr = SynExpr.InterpolatedString(contents = parts; synStringKind = stringKind; range = range)))
+ // method call with interpolated string parameter - c.M($"
{1 + 1}")
+ | SynExpr.App(
+ funcExpr = Ident(ident)
+ argExpr = SynExpr.InterpolatedString(contents = parts; synStringKind = stringKind; range = range)) ->
+ let rangesToRemove =
+ discoverRangesToRemoveForInterpolatedString stringKind (Array.ofList parts)
+
+ Some(
+ [| { methodIdent = ident
+ parameterRange = range
+ rangesToRemove = rangesToRemove
+ parameterPosition = 0 } |]
+ )
+ // piped method call with string parameter - "
" |> c.M
+ // piped method call with interpolated parameter - $"
{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("
", true) and/or c.M(true, "
")
+ // 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 - [
] let html = ""
+ // all of the above but with literals
+ | _ -> None
+
+///
+type private StringParameterFinder() =
+ inherit SyntaxCollectorBase()
+
+ let languages = ResizeArray()
+
+ override _.WalkBinding(binding) =
+ match binding with
+ | SynBinding(expr = 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 array }
+
+let rangeMinusRanges (totalRange: Range) (rangesToRemove: Range array) : Range array =
+ match rangesToRemove with
+ | [||] -> [| totalRange |]
+ | _ ->
+ let mutable returnVal = ResizeArray()
+ let mutable currentStart = totalRange.Start
+
+ for r in rangesToRemove do
+ if currentStart = r.Start then
+ // no gaps, so just advance the current pointer
+ currentStart <- r.End
+ else
+ returnVal.Add(Range.mkRange totalRange.FileName currentStart r.Start)
+ currentStart <- r.End
+
+ // only need to add the final range if there is a gap between where we are and the end of the string
+ if currentStart <> totalRange.End then
+ returnVal.Add(Range.mkRange totalRange.FileName currentStart totalRange.End)
+
+ returnVal.ToArray()
+
+let private parametersThatAreStringSyntax
+ (parameters: StringParameter array, checkResults: FSharpCheckFileResults, text: VolatileFile)
+ : NestedLanguageDocument array Async =
+ async {
+ let returnVal = ResizeArray()
+
+ for p in parameters do
+ logger.info (
+ Log.setMessageI
+ $"Checking parameter: {p.parameterRange.ToString():range} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
+ )
+
+ let lastPart = p.methodIdent[^0]
+ let endOfFinalTextToken = lastPart.idRange.End
+
+ match text.Source.GetLine(endOfFinalTextToken) with
+ | None -> ()
+ | Some lineText ->
+
+ match
+ checkResults.GetSymbolUseAtLocation(
+ endOfFinalTextToken.Line,
+ endOfFinalTextToken.Column,
+ lineText,
+ p.methodIdent |> List.map (fun x -> x.idText)
+ )
+ with
+ | None -> ()
+ | Some usage ->
+ logger.info (
+ Log.setMessageI
+ $"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}"
+ )
+
+ 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[p.parameterPosition]
+
+ logger.info (
+ Log.setMessageI
+ $"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}"
+ )
+
+ 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: VolatileFile) : NestedLanguageDocument array Async =
+ async {
+ let potentialParameters = findParametersForParseTree tyRes.GetAST
+
+ logger.info (
+ Log.setMessageI
+ $"Found {potentialParameters.Length:stringParams} potential parameters in {text.FileName:filename}@{text.Version:version}"
+ )
+
+ for p in potentialParameters do
+ logger.info (
+ Log.setMessageI
+ $"Potential parameter: {p.parameterRange.ToString():range} in member {p.methodIdent.ToString():methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}"
+ )
+
+ let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text)
+
+ logger.info (
+ Log.setMessageI
+ $"Found {actualStringSyntaxParameters.Length:stringParams} actual parameters in {text.FileName:filename}@{text.Version:version}"
+ )
+
+ return actualStringSyntaxParameters
+ }
diff --git a/src/FsAutoComplete.Core/UntypedAstUtils.fs b/src/FsAutoComplete.Core/UntypedAstUtils.fs
index d06bc6af3..7722b0726 100644
--- a/src/FsAutoComplete.Core/UntypedAstUtils.fs
+++ b/src/FsAutoComplete.Core/UntypedAstUtils.fs
@@ -26,11 +26,12 @@ module Syntax =
loop [] pats
+ []
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
@@ -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
diff --git a/src/FsAutoComplete.Core/UntypedAstUtils.fsi b/src/FsAutoComplete.Core/UntypedAstUtils.fsi
index d99122114..40cab0b89 100644
--- a/src/FsAutoComplete.Core/UntypedAstUtils.fsi
+++ b/src/FsAutoComplete.Core/UntypedAstUtils.fsi
@@ -3,36 +3,65 @@ namespace FSharp.Compiler
module Syntax =
open FSharp.Compiler.Syntax
+ []
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
diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs
index c521a4eb7..c5f5dc67c 100644
--- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs
+++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs
@@ -563,19 +563,26 @@ type AdaptiveState
Loggers.analyzers.error (Log.setMessageI $"Run failed for {file:file}" >> Log.addExn ex)
}
+ let checkForNestedLanguages _config parseAndCheckResults (volatileFile: VolatileFile) =
+ async {
+ let! languages = NestedLanguages.findNestedLanguages (parseAndCheckResults, volatileFile)
+
+ notifications.Trigger(
+ NotificationEvent.NestedLanguagesFound(volatileFile.FileName, volatileFile.Version, languages),
+ CancellationToken.None
+ )
+ }
+
do
disposables.Add
<| fileChecked.Publish.Subscribe(fun (parseAndCheck, volatileFile, ct) ->
if volatileFile.Source.Length = 0 then
() // Don't analyze and error on an empty file
else
- async {
- let config = config |> AVal.force
- do! builtInCompilerAnalyzers config volatileFile parseAndCheck
- do! runAnalyzers config parseAndCheck volatileFile
-
- }
- |> Async.StartWithCT ct)
+ let config = config |> AVal.force
+ Async.Start(builtInCompilerAnalyzers config volatileFile parseAndCheck, ct)
+ Async.Start(runAnalyzers config parseAndCheck volatileFile, ct)
+ Async.Start(checkForNestedLanguages config parseAndCheck volatileFile, ct))
let handleCommandEvents (n: NotificationEvent, ct: CancellationToken) =
@@ -784,6 +791,19 @@ type AdaptiveState
{ 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}"
diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs
index 5ab36cd5c..4c41d6f7e 100644
--- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs
+++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs
@@ -3,7 +3,6 @@ namespace FsAutoComplete.Lsp
open FsAutoComplete
open Ionide.LanguageServerProtocol
-open Ionide.LanguageServerProtocol.Types.LspResult
open Ionide.LanguageServerProtocol.Server
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.LspHelpers
@@ -13,6 +12,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) =
@@ -64,6 +71,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 } } } ->
diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi
index 4fbadc76e..0d69ca6bb 100644
--- a/src/FsAutoComplete/LspServers/FSharpLspClient.fsi
+++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fsi
@@ -8,6 +8,14 @@ open System
open System.Threading
open IcedTasks
+type NestedLanguage =
+ { Language: string
+ Ranges: Types.Range[] }
+
+type TextDocumentNestedLanguages =
+ { TextDocument: VersionedTextDocumentIdentifier
+ NestedLanguages: NestedLanguage[] }
+
type FSharpLspClient =
new: sendServerNotification: ClientNotificationSender * sendServerRequest: ClientRequestSender -> FSharpLspClient
inherit LspClient
@@ -32,9 +40,11 @@ type FSharpLspClient =
member NotifyDocumentAnalyzed: p: DocumentAnalyzedNotification -> Async
member NotifyTestDetected: p: TestDetectedNotification -> Async
member CodeLensRefresh: unit -> Async
+ member NotifyNestedLanguages: p: TextDocumentNestedLanguages -> Async
override WorkDoneProgressCreate: ProgressToken -> AsyncLspResult
override Progress: ProgressToken * 'Progress -> Async
+
///
/// Represents a progress report that can be used to report progress to the client.
///
diff --git a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs
new file mode 100644
index 000000000..e8b73772d
--- /dev/null
+++ b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs
@@ -0,0 +1,207 @@
+module FsAutoComplete.Tests.NestedLanguageTests
+
+open Expecto
+open Utils.ServerTests
+open Helpers
+open Utils.Server
+open System
+open Ionide.LanguageServerProtocol.Types
+
+type Document with
+
+ member x.NestedLanguages =
+ x.Server.Events
+ |> Document.typedEvents ("fsharp/textDocument/nestedLanguages")
+ |> Observable.filter (fun n -> n.TextDocument = x.VersionedTextDocumentIdentifier)
+
+let private getDocumentText (lines: string[]) (ranges: Range array) : string =
+ ranges
+ |> Array.map (fun r ->
+ let startLine = lines.[int r.Start.Line]
+ let endLine = lines.[int r.End.Line]
+
+ if r.Start.Line = r.End.Line then
+ startLine.Substring(int r.Start.Character, int (r.End.Character - r.Start.Character))
+ else
+ let start = startLine.Substring(int r.Start.Character)
+ let ``end`` = endLine.Substring(0, int r.End.Character)
+
+ let middle =
+ lines.[int (r.Start.Line + 1u) .. int (r.End.Line - 1u)]
+ |> Array.map (fun l -> l.Trim())
+
+ let middle = String.Join(" ", middle)
+ start + middle + ``end``)
+ |> String.concat "\n"
+
+
+
+let private contentErrorMessage
+ (actual: FsAutoComplete.Lsp.NestedLanguage array)
+ (expected: FsAutoComplete.Lsp.NestedLanguage array)
+ (sourceText: string)
+ =
+ let builder = System.Text.StringBuilder()
+ let lines = sourceText.Split([| '\n'; '\r' |], StringSplitOptions.None)
+
+ builder.AppendLine "Expected nested documents to be equivalent, but found differences"
+ |> ignore
+
+ if actual.Length <> expected.Length then
+ builder.AppendLine $"Expected %d{expected.Length} nested languages, but found %d{actual.Length}"
+ |> ignore
+ else
+ for (index, (expected, actual)) in Array.zip expected actual |> Array.indexed do
+ if expected.Language <> actual.Language then
+ builder.AppendLine
+ $"Expected document #${index}'s language to be %s{expected.Language}, but was %s{actual.Language}"
+ |> ignore
+
+ let expectedText = getDocumentText lines expected.Ranges
+ let actualText = getDocumentText lines actual.Ranges
+
+ builder.AppendLine $"Expected document #{index} to be \n\t%s{expectedText}\nbut was\n\t%s{actualText}"
+ |> ignore
+
+ builder.ToString()
+
+let hasLanguages name source expectedLanguages server =
+ testAsync name {
+ let! (doc, diags) = server |> Server.createUntitledDocument source
+ Expect.isEmpty diags "no diagnostics"
+ let! nestedLanguages = doc.NestedLanguages |> Async.AwaitObservable
+
+ let mappedExpectedLanguages: FsAutoComplete.Lsp.NestedLanguage array =
+ expectedLanguages
+ |> Array.map (fun (l, rs) ->
+ { Language = l
+ Ranges =
+ rs
+ |> Array.map (fun ((sl, sc), (el, ec)) ->
+ { Start = { Line = sl; Character = sc }
+ End = { Line = el; Character = ec } }) })
+
+ Expect.equal
+ nestedLanguages.NestedLanguages
+ mappedExpectedLanguages
+ (contentErrorMessage nestedLanguages.NestedLanguages mappedExpectedLanguages source)
+ }
+
+let tests state =
+ testList
+ "nested languages"
+ [ testList
+ "unsupported scenarios"
+ // pending because class members don't return attributes in the FCS Parameter API
+ [ serverTestList "class member" state defaultConfigDto None (fun server ->
+ [ hasLanguages
+ "BCL type"
+ """
+ let b = System.UriBuilder("https://google.com")
+ """
+ [| ("uri", [| (1u, 38u), (1u, 58u) |]) |]
+ server
+
+ hasLanguages
+ "F#-defined type"
+ """
+ type Foo() =
+ member x.Boo([] uriString: string) = ()
+ let f = new Foo()
+ let u = f.Boo("https://google.com")
+ """
+ [| ("uri", [| (4u, 26u), (4u, 46u) |]) |]
+ server ])
+ serverTestList "functions" state defaultConfigDto None (fun server ->
+ [ hasLanguages
+ "interpolated string with format specifier"
+ """
+ let uri ([]s: string) = ()
+ let u = uri $"https://%b{true}.com"
+ """
+ [| ("uri", [| (2u, 26u), (2u, 34u); (2u, 42u), (2u, 46u) |]) |]
+ server
+
+
+ // commented out because I can't figure out how to get the new string interpolation working
+ // hasLanguages
+ // "more than triple-quoted interpolated string with format specifier"
+ // """
+ // let uri ([]s: string) = ()
+ // let u = uri $$""""https://%b{{true}}.com""""
+ // """
+ // [| ("uri", [| (2, 24), (2, 35); (2, 39), (2, 45) |]) |]
+ // server
+ ]) ]
+ testList
+ "FSharp Code"
+ [ serverTestList "let bound function member" state defaultConfigDto None (fun server ->
+ [ hasLanguages
+ "normal string value"
+ """
+ let boo ([] uriString: string) = ()
+ let u = boo "https://google.com"
+ """
+ // note for reader - 24 is the start quote, 44 is the end quote, so we want a doc including 25-43
+ [| ("uri", [| (2u, 25u), (2u, 43u) |]) |]
+ server
+
+ hasLanguages
+ "verbatim string value"
+ """
+ let boo ([] uriString: string) = ()
+ let u = boo @"https://google.com"
+ """
+ [| ("uri", [| (2u, 26u), (2u, 44u) |]) |]
+ server
+
+ hasLanguages
+ "triple-quote string value"
+ """
+ let boo ([] uriString: string) = ()
+ let u = boo "https://google.com"
+ """
+ [| ("uri", [| (2u, 25u), (2u, 43u) |]) |]
+ server
+
+ hasLanguages
+ "simple interpolated string"
+ """
+ let uri ([]s: string) = ()
+ let u = uri $"https://{true}.com"
+ """
+ [| ("uri", [| (2u, 26u), (2u, 34u); (2u, 40u), (2u, 44u) |]) |]
+ server
+
+ // commented out because I can't figure out how to get the new string interpolation working
+ // hasLanguages
+ // "triple-quote interpolated string"
+ // """
+ // let uri ([]s: string) = ()
+ // let u = uri $\"\"\"https://{true}.com"\"\\"
+ // """
+ // [| ("uri", [| (2, 24), (2, 35); (2, 39), (2, 45) |]) |]
+ // server
+
+
+
+ // commented out because I can't figure out how to get the new string interpolation working
+ // hasLanguages
+ // "triple-quoted interpolated string with format specifier"
+ // """
+ // let uri ([]s: string) = ()
+ // let u = uri $"https://%b{true}.com"
+ // """
+ // [| ("uri", [| (2, 24), (2, 35); (2, 39), (2, 45) |]) |]
+ // server
+
+ hasLanguages
+ "multiple languages in the same document"
+ """
+ let html ([]s: string) = ()
+ let sql ([]s: string) = ()
+ let myWebPage = html "wow"
+ let myQuery = sql "select * from accounts where net_worth > 1000000"
+ """
+ [| ("html", [| (3u, 34u), (3u, 50u) |]); ("sql", [| (4u, 31u), (4u, 79u) |]) |]
+ server ]) ] ]
diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs
index e85198116..4b9fa2b9e 100644
--- a/test/FsAutoComplete.Tests.Lsp/Program.fs
+++ b/test/FsAutoComplete.Tests.Lsp/Program.fs
@@ -53,7 +53,7 @@ let loaders =
let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory =
- Helpers.createAdaptiveServer (fun () -> workspaceLoaderFactory toolsPath) sourceTextFactory
+ Helpers.createAdaptiveServer (fun () -> workspaceLoaderFactory toolsPath) sourceTextFactory true
let sourceTextFactory: ISourceTextFactory = RoslynSourceTextFactory()
@@ -81,69 +81,67 @@ let lspTests =
testList
$"{loaderName}"
[
- for (compilerName, useTransparentCompiler) in compilers do
- testList
- $"{compilerName}"
- [
- Templates.tests ()
- let createServer () =
- adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory useTransparentCompiler
-
- initTests createServer
- closeTests createServer
-
- Utils.Tests.Server.tests createServer
- Utils.Tests.CursorbasedTests.tests createServer
-
- CodeLens.tests createServer
- documentSymbolTest createServer
- workspaceSymbolTest createServer
- Completion.autocompleteTest createServer
- Completion.autoOpenTests createServer
- Completion.fullNameExternalAutocompleteTest createServer
- foldingTests createServer
- tooltipTests createServer
- Highlighting.tests createServer
- scriptPreviewTests createServer
- scriptEvictionTests createServer
- scriptProjectOptionsCacheTests createServer
- dependencyManagerTests createServer
- interactiveDirectivesUnitTests
-
- // commented out because FSDN is down
- //fsdnTest createServer
-
- //linterTests createServer
- uriTests
- formattingTests createServer
- analyzerTests createServer
- signatureTests createServer
- SignatureHelp.tests createServer
- InlineHints.tests createServer
- CodeFixTests.Tests.tests sourceTextFactory createServer
- Completion.tests createServer
- GoTo.tests createServer
-
- FindReferences.tests createServer
- Rename.tests createServer
-
- InfoPanelTests.docFormattingTest createServer
- DetectUnitTests.tests createServer
- XmlDocumentationGeneration.tests createServer
- InlayHintTests.tests createServer
- DependentFileChecking.tests createServer
- UnusedDeclarationsTests.tests createServer
- EmptyFileTests.tests createServer
- CallHierarchy.tests createServer
- diagnosticsTest createServer
- ] ] ]
+ Templates.tests ()
+ let createServer () =
+ adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory
+
+ initTests createServer
+ closeTests createServer
+
+ Utils.Tests.Server.tests createServer
+ Utils.Tests.CursorbasedTests.tests createServer
+
+ CodeLens.tests createServer
+ documentSymbolTest createServer
+ workspaceSymbolTest createServer
+ Completion.autocompleteTest createServer
+ Completion.autoOpenTests createServer
+ Completion.fullNameExternalAutocompleteTest createServer
+ foldingTests createServer
+ tooltipTests createServer
+ Highlighting.tests createServer
+ scriptPreviewTests createServer
+ scriptEvictionTests createServer
+ scriptProjectOptionsCacheTests createServer
+ dependencyManagerTests createServer
+ interactiveDirectivesUnitTests
+
+ // commented out because FSDN is down
+ //fsdnTest createServer
+
+ //linterTests createServer
+ uriTests
+ formattingTests createServer
+ analyzerTests createServer
+ signatureTests createServer
+ SignatureHelp.tests createServer
+ InlineHints.tests createServer
+ CodeFixTests.Tests.tests sourceTextFactory createServer
+ Completion.tests createServer
+ GoTo.tests createServer
+
+ FindReferences.tests createServer
+ Rename.tests createServer
+
+ InfoPanelTests.docFormattingTest createServer
+ DetectUnitTests.tests createServer
+ XmlDocumentationGeneration.tests createServer
+ InlayHintTests.tests createServer
+ DependentFileChecking.tests createServer
+ UnusedDeclarationsTests.tests createServer
+ EmptyFileTests.tests createServer
+ CallHierarchy.tests createServer
+ NestedLanguageTests.tests createServer
+ diagnosticsTest createServer
+ ] ]
/// Tests that do not require a LSP server
-let generalTests = testList "general" [
- testList (nameof (Utils)) [ Utils.Tests.Utils.tests; Utils.Tests.TextEdit.tests ]
- InlayHintTests.explicitTypeInfoTests sourceTextFactory
- FindReferences.tryFixupRangeTests sourceTextFactory
-]
+let generalTests =
+ testList
+ "general"
+ [ testList (nameof (Utils)) [ Utils.Tests.Utils.tests; Utils.Tests.TextEdit.tests ]
+ InlayHintTests.explicitTypeInfoTests sourceTextFactory
+ FindReferences.tryFixupRangeTests sourceTextFactory ]
[]
let tests = testList "FSAC" [
diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs
index 065617682..62d6e77d1 100644
--- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs
+++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs
@@ -85,8 +85,7 @@ module Server =
match! server.Initialize p with
| Ok _ ->
- do! server.Initialized()
-
+ do! server.Initialized ()
return
{ RootPath = path
Server = server
@@ -200,8 +199,8 @@ module Document =
open System.Reactive.Linq
open System.Threading.Tasks
- let private typedEvents<'t> typ : _ -> System.IObservable<'t> =
- Observable.choose (fun (typ', _o) -> if typ' = typ then Some(unbox _o) else None)
+ let typedEvents<'t> eventName : _ -> System.IObservable<'t> =
+ Observable.choose (fun (typ', _o) -> if typ' = eventName then Some(unbox _o) else None)
/// `textDocument/publishDiagnostics`
///
diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fsi b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fsi
index e31695c57..6c788e9ef 100644
--- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fsi
+++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fsi
@@ -16,111 +16,113 @@ open Utils
open Ionide.ProjInfo.Logging
type Server =
- { RootPath: string option
- Server: IFSharpLspServer
- Events: ClientEvents
- mutable UntitledCounter: int }
+ { RootPath: string option
+ Server: IFSharpLspServer
+ Events: ClientEvents
+ mutable UntitledCounter: int }
/// `Server` cached with `Async.Cache`
type CachedServer = Async
type Document =
- { Server: Server
- FilePath: string
- Uri: DocumentUri
- mutable Version: int }
+ { Server: Server
+ FilePath: string
+ Uri: DocumentUri
+ mutable Version: int }
- member TextDocumentIdentifier: TextDocumentIdentifier
- member VersionedTextDocumentIdentifier: VersionedTextDocumentIdentifier
- member Diagnostics: IObservable
- member CompilerDiagnostics: IObservable
- interface IDisposable
+ member TextDocumentIdentifier: TextDocumentIdentifier
+ member VersionedTextDocumentIdentifier: VersionedTextDocumentIdentifier
+ member Diagnostics: IObservable
+ member CompilerDiagnostics: IObservable
+ interface IDisposable
module Server =
- val create:
- path: string option ->
- config: FSharpConfigDto ->
- createServer: (unit -> IFSharpLspServer * IObservable) ->
- CachedServer
+ val create:
+ path: string option ->
+ config: FSharpConfigDto ->
+ createServer: (unit -> IFSharpLspServer * IObservable) ->
+ CachedServer
- val shutdown: server: CachedServer -> Async
- val createUntitledDocument: initialText: string -> server: CachedServer -> Async
- /// `path` can be absolute or relative.
- /// For relative path `server.RootPath` must be specified!
- ///
- /// Note: When `path` is relative: relative to `server.RootPath`!
- val openDocument: path: string -> server: CachedServer -> Async
+ val shutdown: server: CachedServer -> Async
+ val createUntitledDocument: initialText: string -> server: CachedServer -> Async
+ /// `path` can be absolute or relative.
+ /// For relative path `server.RootPath` must be specified!
+ ///
+ /// Note: When `path` is relative: relative to `server.RootPath`!
+ val openDocument: path: string -> server: CachedServer -> Async
- /// Like `Server.openDocument`, but instead of reading source text from `path`,
- /// this here instead uses `initialText` (which can be different from content of `path`!).
- ///
- /// This way an existing file with different text can be faked.
- /// Logically equal to `Server.openDocument`, and later changing its text via `Document.changeTextTo`.
- /// But this here doesn't have to parse and check everything twice (once for open, once for changed)
- /// and is WAY faster than `Server.openDocument` followed by `Document.changeTextTo` when involving multiple documents.
- /// (For example with CodeFix tests using `fsi` file and corresponding `fs` file)
- val openDocumentWithText:
- path: string -> initialText: string -> server: CachedServer -> Async
+ /// Like `Server.openDocument`, but instead of reading source text from `path`,
+ /// this here instead uses `initialText` (which can be different from content of `path`!).
+ ///
+ /// This way an existing file with different text can be faked.
+ /// Logically equal to `Server.openDocument`, and later changing its text via `Document.changeTextTo`.
+ /// But this here doesn't have to parse and check everything twice (once for open, once for changed)
+ /// and is WAY faster than `Server.openDocument` followed by `Document.changeTextTo` when involving multiple documents.
+ /// (For example with CodeFix tests using `fsi` file and corresponding `fs` file)
+ val openDocumentWithText:
+ path: string -> initialText: string -> server: CachedServer -> Async
module Document =
- open System.Reactive.Linq
- open System.Threading.Tasks
+ open System.Reactive.Linq
+ open System.Threading.Tasks
- /// `textDocument/publishDiagnostics`
- ///
- /// Note: for each analyzing round there are might be multiple `publishDiagnostics` events (F# compiler, for each built-in Analyzer, for Custom Analyzers)
- ///
- /// Note: Because source `doc.Server.Events` is `ReplaySubject`, subscribing to Stream returns ALL past diagnostics too!
- val diagnosticsStream: doc: Document -> IObservable
- /// `fsharp/documentAnalyzed`
- val analyzedStream: doc: Document -> IObservable
- /// in ms
- /// Waits (if necessary) and gets latest diagnostics.
- ///
- /// To detect newest diags:
- /// * Waits for `fsharp/documentAnalyzed` for passed `doc` and its `doc.Version`.
- /// * Then waits a but more for potential late diags.
- /// * Then returns latest diagnostics.
- ///
- ///
- /// ### Explanation: Get latest & correct diagnostics
- /// Diagnostics aren't collected and then sent once, but instead sent after each parsing/analyzing step.
- /// -> There are multiple `textDocument/publishDiagnostics` sent for each parsing/analyzing round:
- /// * one when file parsed by F# compiler
- /// * one for each built-in (enabled) Analyzers (in `src\FsAutoComplete\FsAutoComplete.Lsp.fs` > `FsAutoComplete.Lsp.FSharpLspServer.analyzeFile`),
- /// * for linter (currently disabled)
- /// * for custom analyzers
- ///
- /// -> To receive ALL diagnostics: use Diagnostics of last `textDocument/publishDiagnostics` event.
- ///
- /// Issue: What is the last `publishDiagnostics`? Might already be here or arrive in future.
- /// -> `fsharp/documentAnalyzed` was introduced. Notification when a doc was completely analyzed
- /// -> wait for `documentAnalyzed`
- ///
- /// But issue: last `publishDiagnostics` might be received AFTER `documentAnalyzed` (because of async notifications & sending)
- /// -> after receiving `documentAnalyzed` wait a bit for late `publishDiagnostics`
- ///
- /// But issue: Wait for how long? Too long: extends test execution time. Too short: Might miss diags.
- /// -> unresolved. Current wait based on testing on modern_ish PC. Seems to work on CI too.
- ///
- ///
- /// *Inconvenience*: Only newest diags can be retrieved this way. Diags for older file versions cannot be extracted reliably:
- /// `doc.Server.Events` is a `ReplaySubject` -> returns ALL previous events on new subscription
- /// -> All past `documentAnalyzed` events and their diags are all received at once
- /// -> waiting a bit after a version-specific `documentAnalyzed` always returns latest diags.
- val waitForLatestDiagnostics: timeout: TimeSpan -> doc: Document -> Async
- val openWith: initialText: string -> doc: Document -> Async
- val close: doc: Document -> Async
- ///
- /// Fire a textDocument/didChange request for the specified document with the given text
- /// as the entire new text of the document, then wait for diagnostics for the document.
- ///
- val changeTextTo: text: string -> doc: Document -> Async
- val saveText: text: string -> doc: Document -> Async
+ val typedEvents: eventName: string -> (ClientEvents -> IObservable<'t>)
- /// Note: diagnostics aren't filtered to match passed range in here
- val codeActionAt:
- diagnostics: Diagnostic[] -> range: Range -> doc: Document -> Async
+ /// `textDocument/publishDiagnostics`
+ ///
+ /// Note: for each analyzing round there are might be multiple `publishDiagnostics` events (F# compiler, for each built-in Analyzer, for Custom Analyzers)
+ ///
+ /// Note: Because source `doc.Server.Events` is `ReplaySubject`, subscribing to Stream returns ALL past diagnostics too!
+ val diagnosticsStream: doc: Document -> IObservable
+ /// `fsharp/documentAnalyzed`
+ val analyzedStream: doc: Document -> IObservable
+ /// in ms
+ /// Waits (if necessary) and gets latest diagnostics.
+ ///
+ /// To detect newest diags:
+ /// * Waits for `fsharp/documentAnalyzed` for passed `doc` and its `doc.Version`.
+ /// * Then waits a but more for potential late diags.
+ /// * Then returns latest diagnostics.
+ ///
+ ///
+ /// ### Explanation: Get latest & correct diagnostics
+ /// Diagnostics aren't collected and then sent once, but instead sent after each parsing/analyzing step.
+ /// -> There are multiple `textDocument/publishDiagnostics` sent for each parsing/analyzing round:
+ /// * one when file parsed by F# compiler
+ /// * one for each built-in (enabled) Analyzers (in `src\FsAutoComplete\FsAutoComplete.Lsp.fs` > `FsAutoComplete.Lsp.FSharpLspServer.analyzeFile`),
+ /// * for linter (currently disabled)
+ /// * for custom analyzers
+ ///
+ /// -> To receive ALL diagnostics: use Diagnostics of last `textDocument/publishDiagnostics` event.
+ ///
+ /// Issue: What is the last `publishDiagnostics`? Might already be here or arrive in future.
+ /// -> `fsharp/documentAnalyzed` was introduced. Notification when a doc was completely analyzed
+ /// -> wait for `documentAnalyzed`
+ ///
+ /// But issue: last `publishDiagnostics` might be received AFTER `documentAnalyzed` (because of async notifications & sending)
+ /// -> after receiving `documentAnalyzed` wait a bit for late `publishDiagnostics`
+ ///
+ /// But issue: Wait for how long? Too long: extends test execution time. Too short: Might miss diags.
+ /// -> unresolved. Current wait based on testing on modern_ish PC. Seems to work on CI too.
+ ///
+ ///
+ /// *Inconvenience*: Only newest diags can be retrieved this way. Diags for older file versions cannot be extracted reliably:
+ /// `doc.Server.Events` is a `ReplaySubject` -> returns ALL previous events on new subscription
+ /// -> All past `documentAnalyzed` events and their diags are all received at once
+ /// -> waiting a bit after a version-specific `documentAnalyzed` always returns latest diags.
+ val waitForLatestDiagnostics: timeout: TimeSpan -> doc: Document -> Async
+ val openWith: initialText: string -> doc: Document -> Async
+ val close: doc: Document -> Async
+ ///
+ /// Fire a textDocument/didChange request for the specified document with the given text
+ /// as the entire new text of the document, then wait for diagnostics for the document.
+ ///
+ val changeTextTo: text: string -> doc: Document -> Async
+ val saveText: text: string -> doc: Document -> Async
- val inlayHintsAt: range: Range -> doc: Document -> Async
- val resolveInlayHint: inlayHint: InlayHint -> doc: Document -> Async
+ /// Note: diagnostics aren't filtered to match passed range in here
+ val codeActionAt:
+ diagnostics: Diagnostic[] -> range: Range -> doc: Document -> Async
+
+ val inlayHintsAt: range: Range -> doc: Document -> Async
+ val resolveInlayHint: inlayHint: InlayHint -> doc: Document -> Async