From f4141472a5d00a72177dcbde1ed0d4c6f9229633 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sat, 25 Nov 2023 15:57:09 -0600 Subject: [PATCH 01/12] initial nested language support --- src/FsAutoComplete.Core/Commands.fs | 4 + .../FsAutoComplete.Core.fsproj | 2 + src/FsAutoComplete.Core/NestedLanguages.fs | 190 ++++++++++++++++++ src/FsAutoComplete.Core/UntypedAstUtils.fs | 7 +- src/FsAutoComplete.Core/UntypedAstUtils.fsi | 29 +++ .../LspServers/AdaptiveServerState.fs | 13 ++ .../LspServers/FSharpLspClient.fs | 13 +- .../NestedLanguageTests.fs | 45 +++++ test/FsAutoComplete.Tests.Lsp/Program.fs | 22 +- test/FsAutoComplete.Tests.Lsp/Utils/Server.fs | 125 +++++------- .../FsAutoComplete.Tests.Lsp/Utils/Server.fsi | 188 ++++++++--------- 11 files changed, 463 insertions(+), 175 deletions(-) create mode 100644 src/FsAutoComplete.Core/NestedLanguages.fs create mode 100644 test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs diff --git a/src/FsAutoComplete.Core/Commands.fs b/src/FsAutoComplete.Core/Commands.fs index 7b1e34125..a34972c5b 100644 --- a/src/FsAutoComplete.Core/Commands.fs +++ b/src/FsAutoComplete.Core/Commands.fs @@ -83,6 +83,10 @@ type NotificationEvent = | Canceled of errorMessage: string | FileParsed of string | TestDetected of file: string * tests: TestAdapter.TestAdapterEntry[] + | NestedLanguagesFound of + file: string * + version: int * + nestedLanguages: NestedLanguages.NestedLanguageDocument array module Commands = open System.Collections.Concurrent diff --git a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj index 81df0fe33..50e6a8535 100644 --- a/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj +++ b/src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj @@ -4,6 +4,7 @@ net6.0;net7.0 net6.0;net7.0;net8.0 false + preview @@ -58,6 +59,7 @@ + diff --git a/src/FsAutoComplete.Core/NestedLanguages.fs b/src/FsAutoComplete.Core/NestedLanguages.fs new file mode 100644 index 000000000..f88cabf1c --- /dev/null +++ b/src/FsAutoComplete.Core/NestedLanguages.fs @@ -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 = + match e with + // lines inside a binding + // let doThing () = + // c.M("
") + // c.M($"
{1 + 1}") + // "
" |> c.M + // $"
{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("
") + | 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 = [||] + parameterPosition = 0 } |] + ) + // method call with interpolated string parameter - c.M $"
{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($"
{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 - "
" |> 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(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 = + 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 + } diff --git a/src/FsAutoComplete.Core/UntypedAstUtils.fs b/src/FsAutoComplete.Core/UntypedAstUtils.fs index 17c923bcc..9a28adcea 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 f5d829512..8b78c1343 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -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}" diff --git a/src/FsAutoComplete/LspServers/FSharpLspClient.fs b/src/FsAutoComplete/LspServers/FSharpLspClient.fs index cbd1e2fcb..a5d2df915 100644 --- a/src/FsAutoComplete/LspServers/FSharpLspClient.fs +++ b/src/FsAutoComplete/LspServers/FSharpLspClient.fs @@ -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 @@ -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) = @@ -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 } } } -> diff --git a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs new file mode 100644 index 000000000..8f56f33f4 --- /dev/null +++ b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs @@ -0,0 +1,45 @@ +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 hasLanguages name source expectedLanguages server = + testAsync name { + let! (doc, _) = server |> Server.createUntitledDocument source + 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 "languages" + } + +let tests state = + testList + "nested languages" + [ serverTestList "class member" state defaultConfigDto None (fun server -> + [ hasLanguages + "with single string parameter" + """ + let b = System.UriBuilder("https://google.com") + """ + [| ("uri", [| (1, 38), (1, 58) |]) |] + server ]) ] diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 9e09c51e9..34846e7b9 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -32,10 +32,13 @@ let testTimeout = Environment.SetEnvironmentVariable("FSAC_WORKSPACELOAD_DELAY", "250") let loaders = + [ "Ionide WorkspaceLoader", + (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) [ "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] + ] let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory = @@ -95,15 +98,16 @@ let lspTests = 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 - ] ] + 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 + ] ] /// Tests that do not require a LSP server let generalTests = testList "general" [ diff --git a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs index 79048a600..0f13d2d99 100644 --- a/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs +++ b/test/FsAutoComplete.Tests.Lsp/Utils/Server.fs @@ -28,22 +28,19 @@ type CachedServer = Async type Document = { Server: Server - FilePath : string + FilePath: string Uri: DocumentUri mutable Version: int } + member doc.TextDocumentIdentifier: TextDocumentIdentifier = { Uri = doc.Uri } member doc.VersionedTextDocumentIdentifier: VersionedTextDocumentIdentifier = - { Uri = doc.Uri - Version = doc.Version } + { Uri = doc.Uri; Version = doc.Version } member x.Diagnostics = - x.Server.Events - |> fileDiagnosticsForUri x.TextDocumentIdentifier.Uri + x.Server.Events |> fileDiagnosticsForUri x.TextDocumentIdentifier.Uri - member x.CompilerDiagnostics = - x.Diagnostics - |> diagnosticsFromSource "F# Compiler" + member x.CompilerDiagnostics = x.Diagnostics |> diagnosticsFromSource "F# Compiler" interface IDisposable with override doc.Dispose() : unit = @@ -65,7 +62,7 @@ module Server = for file in System.IO.Directory.EnumerateFiles(path, "*.fsproj", SearchOption.AllDirectories) do do! file |> Path.GetDirectoryName |> dotnetRestore - let (server : IFSharpLspServer, events : IObservable<_>) = createServer () + let (server: IFSharpLspServer, events: IObservable<_>) = createServer () events |> Observable.add logEvent let p: InitializeParams = @@ -88,7 +85,8 @@ module Server = match! server.Initialize p with | Ok _ -> - do! server.Initialized (InitializedParams()) + do! server.Initialized(InitializedParams()) + return { RootPath = path Server = server @@ -131,9 +129,7 @@ module Server = async { let! server = server - let doc = - server - |> createDocument String.Empty (server |> nextUntitledDocUri) + let doc = server |> createDocument String.Empty (server |> nextUntitledDocUri) let! diags = doc |> Document.openWith initialText @@ -161,17 +157,13 @@ module Server = server |> createDocument fullPath - ( - fullPath - // normalize path is necessary: otherwise might be different lower/upper cases in uri for tests and LSP server: - // on windows `E:\...`: `file:///E%3A/...` (before normalize) vs. `file:///e%3A/..` (after normalize) - |> normalizePath - |> Path.LocalPathToUri - ) - - let! diags = - doc - |> Document.openWith (File.ReadAllText fullPath) + (fullPath + // normalize path is necessary: otherwise might be different lower/upper cases in uri for tests and LSP server: + // on windows `E:\...`: `file:///E%3A/...` (before normalize) vs. `file:///e%3A/..` (after normalize) + |> normalizePath + |> Path.LocalPathToUri) + + let! diags = doc |> Document.openWith (File.ReadAllText fullPath) return (doc, diags) } @@ -197,9 +189,7 @@ module Server = // To avoid hitting the typechecker cache, we need to update the file's timestamp IO.File.SetLastWriteTimeUtc(fullPath, DateTime.UtcNow) - let doc = - server - |> createDocument fullPath (Path.FilePathToUri fullPath) + let doc = server |> createDocument fullPath (Path.FilePathToUri fullPath) let! diags = doc |> Document.openWith initialText @@ -210,12 +200,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 : Helpers.ClientEvents -> System.IObservable<'t> = + Observable.choose (fun (typ', _o) -> if typ' = eventName then Some(unbox _o) else None) /// `textDocument/publishDiagnostics` /// @@ -225,11 +211,7 @@ module Document = let diagnosticsStream (doc: Document) = doc.Server.Events |> typedEvents "textDocument/publishDiagnostics" - |> Observable.choose (fun n -> - if n.Uri = doc.Uri then - Some n.Diagnostics - else - None) + |> Observable.choose (fun n -> if n.Uri = doc.Uri then Some n.Diagnostics else None) /// `fsharp/documentAnalyzed` let analyzedStream (doc: Document) = @@ -241,21 +223,19 @@ module Document = /// in ms let private waitForLateDiagnosticsDelay = let envVar = "FSAC_WaitForLateDiagnosticsDelay" + System.Environment.GetEnvironmentVariable envVar |> Option.ofObj |> Option.map (fun d -> match System.Int32.TryParse d with | (true, d) -> d - | (false, _) -> - failwith $"Environment Variable '%s{envVar}' exists, but is not a correct int number ('%s{d}')" - ) + | (false, _) -> failwith $"Environment Variable '%s{envVar}' exists, but is not a correct int number ('%s{d}')") |> Option.orElseWith (fun _ -> - // set in Github Actions: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables - match System.Environment.GetEnvironmentVariable "CI" with - | null -> None - | _ -> Some 25 - ) - |> Option.defaultValue 7 // testing locally + // set in Github Actions: https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables + match System.Environment.GetEnvironmentVariable "CI" with + | null -> None + | _ -> Some 25) + |> Option.defaultValue 7 // testing locally /// Waits (if necessary) and gets latest diagnostics. /// @@ -298,6 +278,7 @@ module Document = >> Log.addContext "uri" doc.Uri >> Log.addContext "version" doc.Version ) + let tcs = TaskCompletionSource<_>() use _ = @@ -313,7 +294,7 @@ module Document = ) |> Observable.bufferSpan (timeout) // |> Observable.timeoutSpan timeout - |> Observable.subscribe(fun x -> tcs.SetResult x) + |> Observable.subscribe (fun x -> tcs.SetResult x) let! result = tcs.Task |> Async.AwaitTask @@ -326,7 +307,7 @@ module Document = System.Threading.Interlocked.Increment(&doc.Version) /// Note: Mutates passed `doc` - let private incrVersionedTextDocumentIdentifier (doc: Document): VersionedTextDocumentIdentifier = + let private incrVersionedTextDocumentIdentifier (doc: Document) : VersionedTextDocumentIdentifier = { Uri = doc.Uri Version = incrVersion doc } @@ -343,8 +324,8 @@ module Document = try return! doc |> waitForLatestDiagnostics Helpers.defaultTimeout - with - | :? TimeoutException -> return failwith $"Timeout waiting for latest diagnostics for {doc.Uri}" + with :? TimeoutException -> + return failwith $"Timeout waiting for latest diagnostics for {doc.Uri}" } let close (doc: Document) = @@ -371,12 +352,11 @@ module Document = return! doc |> waitForLatestDiagnostics Helpers.defaultTimeout } - let saveText (text : string) (doc : Document) = + let saveText (text: string) (doc: Document) = async { - let p : DidSaveTextDocumentParams = { - Text = Some text - TextDocument = doc.TextDocumentIdentifier - } + let p: DidSaveTextDocumentParams = + { Text = Some text + TextDocument = doc.TextDocumentIdentifier } // Simulate the file being written to disk so we don't hit the typechecker cache IO.File.SetLastWriteTimeUtc(doc.FilePath, DateTime.UtcNow) do! doc.Server.Server.TextDocumentDidSave p @@ -387,8 +367,7 @@ module Document = let private assertOk result = Expect.isOk result "Expected success" - result - |> Result.defaultWith (fun _ -> failtest "not reachable") + result |> Result.defaultWith (fun _ -> failtest "not reachable") let private assertSome opt = Expect.isSome opt "Expected to have Some" @@ -401,21 +380,27 @@ module Document = let ps: CodeActionParams = { TextDocument = doc.TextDocumentIdentifier Range = range - Context = { Diagnostics = diagnostics; Only = None; TriggerKind = None } } + Context = + { Diagnostics = diagnostics + Only = None + TriggerKind = None } } let! res = doc.Server.Server.TextDocumentCodeAction ps return res |> assertOk } - let inlayHintsAt range (doc: Document) = async { - let ps: InlayHintParams = { - Range = range - TextDocument = doc.TextDocumentIdentifier + let inlayHintsAt range (doc: Document) = + async { + let ps: InlayHintParams = + { Range = range + TextDocument = doc.TextDocumentIdentifier } + + let! res = doc.Server.Server.TextDocumentInlayHint ps + return res |> assertOk |> assertSome + } + + let resolveInlayHint inlayHint (doc: Document) = + async { + let! res = doc.Server.Server.InlayHintResolve inlayHint + return res |> assertOk } - let! res = doc.Server.Server.TextDocumentInlayHint ps - return res |> assertOk |> assertSome - } - let resolveInlayHint inlayHint (doc: Document) = async { - let! res = doc.Server.Server.InlayHintResolve inlayHint - return res |> assertOk - } 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 From f235f1917f23e8d218ec7aee487fc2619a8fb1a6 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sat, 25 Nov 2023 16:04:34 -0600 Subject: [PATCH 02/12] fix bad merge --- test/FsAutoComplete.Tests.Lsp/Program.fs | 42 ++++++++++-------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 34846e7b9..37a50a4ca 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -32,14 +32,10 @@ let testTimeout = Environment.SetEnvironmentVariable("FSAC_WORKSPACELOAD_DELAY", "250") let loaders = - [ "Ionide WorkspaceLoader", - (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) [ "Ionide WorkspaceLoader", (fun toolpath -> WorkspaceLoader.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) // "MSBuild Project Graph WorkspaceLoader", (fun toolpath -> WorkspaceLoaderViaProjectGraph.Create(toolpath, FsAutoComplete.Core.ProjectLoader.globalProperties)) ] - ] - let adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory = Helpers.createAdaptiveServer (fun () -> workspaceLoaderFactory toolsPath) sourceTextFactory @@ -56,10 +52,8 @@ let lspTests = testList $"{loaderName}" - [ - Templates.tests () - let createServer () = - adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory + [ Templates.tests () + let createServer () = adaptiveLspServerFactory toolsPath workspaceLoaderFactory sourceTextFactory initTests createServer closeTests createServer @@ -94,27 +88,25 @@ let lspTests = 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 - ] ] + 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 ] ] /// 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" [ generalTests; lspTests ] From 6b87f6bbe1993b5fd16876ea07829d3969a1c250 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 26 Nov 2023 11:21:44 -0600 Subject: [PATCH 03/12] actually hook up nested language events and add more tests --- src/FsAutoComplete.Core/NestedLanguages.fs | 28 +++++++++++++++-- .../LspServers/AdaptiveServerState.fs | 21 ++++++++----- .../NestedLanguageTests.fs | 30 ++++++++++++++----- test/FsAutoComplete.Tests.Lsp/Program.fs | 2 +- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/FsAutoComplete.Core/NestedLanguages.fs b/src/FsAutoComplete.Core/NestedLanguages.fs index f88cabf1c..77e94089b 100644 --- a/src/FsAutoComplete.Core/NestedLanguages.fs +++ b/src/FsAutoComplete.Core/NestedLanguages.fs @@ -1,5 +1,6 @@ module FsAutoComplete.NestedLanguages +open FsAutoComplete.Logging open FsToolkit.ErrorHandling open FSharp.Compiler.Syntax open FSharp.Compiler.Text @@ -8,6 +9,8 @@ open FSharp.Compiler.Symbols #nowarn "57" // from-end slicing +let logger = LogProvider.getLoggerByName "NestedLanguages" + type private StringParameter = { methodIdent: LongIdent parameterRange: Range @@ -164,7 +167,7 @@ let private parametersThatAreStringSyntax match sym with | :? FSharpMemberOrFunctionOrValue as mfv -> let allParameters = mfv.CurriedParameterGroups |> Seq.collect id |> Seq.toArray - let fsharpP = allParameters |> Seq.item p.parameterPosition + let fsharpP = allParameters[p.parameterPosition] match fsharpP.Attributes |> Seq.tryPick (|IsStringSyntax|_|) with | Some language -> @@ -181,10 +184,29 @@ let private parametersThatAreStringSyntax /// * 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 = +let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : NestedLanguageDocument[] Async = async { // get all string constants let potentialParameters = findParametersForParseTree tyRes.GetAST - let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text) + + 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:range} in member {p.methodIdent:methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}" + ) + + let! actualStringSyntaxParameters = + parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text.Source) + + logger.info ( + Log.setMessageI + $"Found {actualStringSyntaxParameters.Length:stringParams} actual parameters in {text.FileName:filename}@{text.Version:version}" + ) + return actualStringSyntaxParameters } diff --git a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs index 8b78c1343..2d1e187b2 100644 --- a/src/FsAutoComplete/LspServers/AdaptiveServerState.fs +++ b/src/FsAutoComplete/LspServers/AdaptiveServerState.fs @@ -392,19 +392,26 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac 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) = diff --git a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs index 8f56f33f4..9c4d42b85 100644 --- a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs @@ -16,7 +16,8 @@ type Document with let hasLanguages name source expectedLanguages server = testAsync name { - let! (doc, _) = server |> Server.createUntitledDocument source + let! (doc, diags) = server |> Server.createUntitledDocument source + Expect.isEmpty diags "no diagnostics" let! nestedLanguages = doc.NestedLanguages |> Async.AwaitObservable let mappedExpectedLanguages: FsAutoComplete.Lsp.NestedLanguage array = @@ -35,11 +36,26 @@ let hasLanguages name source expectedLanguages server = let tests state = testList "nested languages" - [ serverTestList "class member" state defaultConfigDto None (fun server -> - [ hasLanguages - "with single string parameter" - """ + [ testList + "BCL" + [ serverTestList "class member" state defaultConfigDto None (fun server -> + [ hasLanguages + "with single string parameter" + """ let b = System.UriBuilder("https://google.com") """ - [| ("uri", [| (1, 38), (1, 58) |]) |] - server ]) ] + [| ("uri", [| (1, 38), (1, 58) |]) |] + server ]) ] + ftestList + "FSharp Code" + [ serverTestList "class member" state defaultConfigDto None (fun server -> + [ hasLanguages + "with single string parameter" + """ + type Foo() = + static member Uri([] uriString: string) = () + + let u = Foo.Uri("https://google.com") + """ + [| ("uri", [| (5, 31), (5, 51) |]) |] + server ]) ] ] diff --git a/test/FsAutoComplete.Tests.Lsp/Program.fs b/test/FsAutoComplete.Tests.Lsp/Program.fs index 37a50a4ca..8e1b23b91 100644 --- a/test/FsAutoComplete.Tests.Lsp/Program.fs +++ b/test/FsAutoComplete.Tests.Lsp/Program.fs @@ -217,7 +217,7 @@ let main args = let cts = new CancellationTokenSource(testTimeout) let args = - [ CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) + [ //CLIArguments.Printer(Expecto.Impl.TestPrinters.summaryWithLocationPrinter defaultConfig.printer) CLIArguments.Verbosity logLevel // CLIArguments.Parallel ] From 80bcbc97b0e37447fbef9ec5f90bf1cd54124a89 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 14:30:45 -0600 Subject: [PATCH 04/12] more WIP testing --- src/FsAutoComplete.Core/NestedLanguages.fs | 39 ++++++++++++------- .../NestedLanguageTests.fs | 16 ++++++-- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/FsAutoComplete.Core/NestedLanguages.fs b/src/FsAutoComplete.Core/NestedLanguages.fs index 77e94089b..cadb8c5fe 100644 --- a/src/FsAutoComplete.Core/NestedLanguages.fs +++ b/src/FsAutoComplete.Core/NestedLanguages.fs @@ -26,6 +26,7 @@ let discoverRangesToRemoveForInterpolatedString (list: SynInterpolatedStringPart let private (|Ident|_|) (e: SynExpr) = match e with + | SynExpr.Ident(ident) -> Some([ ident ]) | SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) -> Some ident | _ -> None @@ -61,12 +62,10 @@ let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option{1 + 1}" | SynExpr.App( - funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) + funcExpr = Ident(ident) argExpr = SynExpr.Paren(expr = SynExpr.InterpolatedString(contents = parts; range = range))) // method call with interpolated string parameter - c.M($"
{1 + 1}") - | SynExpr.App( - funcExpr = SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) - argExpr = SynExpr.InterpolatedString(contents = parts; range = range)) -> + | SynExpr.App(funcExpr = Ident(ident); argExpr = SynExpr.InterpolatedString(contents = parts; range = range)) -> let rangesToRemove = discoverRangesToRemoveForInterpolatedString parts Some( @@ -90,11 +89,12 @@ type private StringParameterFinder() = let languages = ResizeArray() - override _.WalkBinding(SynBinding(expr = expr)) = - match expr with - | IsApplicationWithStringParameters(stringParameters) -> languages.AddRange stringParameters + override _.WalkBinding(binding) = + match binding with + | SynBinding(expr = IsApplicationWithStringParameters(stringParameters)) -> languages.AddRange stringParameters | _ -> () + override _.WalkSynModuleDecl(decl) = match decl with | SynModuleDecl.Expr(expr = IsApplicationWithStringParameters(stringParameters)) -> @@ -137,29 +137,38 @@ let private parametersThatAreStringSyntax ( parameters: StringParameter[], checkResults: FSharpCheckFileResults, - text: IFSACSourceText + text: VolatileFile ) : 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 precedingParts, lastPart = p.methodIdent.[0..^1], p.methodIdent[^0] let endOfFinalTextToken = lastPart.idRange.End - match text.GetLine(endOfFinalTextToken) with + match text.Source.GetLine(endOfFinalTextToken) with | None -> () | Some lineText -> match checkResults.GetSymbolUseAtLocation( endOfFinalTextToken.Line, - endOfFinalTextToken.Column, + endOfFinalTextToken.Column - 1, lineText, precedingParts |> List.map (fun i -> i.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 @@ -169,6 +178,11 @@ let private parametersThatAreStringSyntax 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 @@ -197,11 +211,10 @@ let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : Nest for p in potentialParameters do logger.info ( Log.setMessageI - $"Potential parameter: {p.parameterRange:range} in member {p.methodIdent:methodName} of {text.FileName:filename}@{text.Version:version} -> {text.Source[p.parameterRange]:sourceText}" + $"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.Source) + let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text) logger.info ( Log.setMessageI diff --git a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs index 9c4d42b85..3517becb3 100644 --- a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs @@ -46,16 +46,26 @@ let tests state = """ [| ("uri", [| (1, 38), (1, 58) |]) |] server ]) ] - ftestList + testList "FSharp Code" [ serverTestList "class member" state defaultConfigDto None (fun server -> [ hasLanguages "with single string parameter" """ type Foo() = - static member Uri([] uriString: string) = () + member x.Uri([] uriString: string) = () + let f = new Foo() + let u = f.Uri("https://google.com") + """ + [| ("uri", [| (5, 31), (5, 51) |]) |] + server ]) - let u = Foo.Uri("https://google.com") + fserverTestList "let bound function member" state defaultConfigDto None (fun server -> + [ hasLanguages + "with single string parameter" + """ + let foo ([] uriString: string) = () + let u = foo "https://google.com" """ [| ("uri", [| (5, 31), (5, 51) |]) |] server ]) ] ] From 0f2d91857ba3a5c4bb6028bf73aa54e8477e3ace Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 16:15:10 -0600 Subject: [PATCH 05/12] Enable support for tagging single-string-parameter functions' arguments as that function's named language --- src/FsAutoComplete.Core/NestedLanguages.fs | 73 ++++++++++++++++++- .../NestedLanguageTests.fs | 39 +++++++++- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/FsAutoComplete.Core/NestedLanguages.fs b/src/FsAutoComplete.Core/NestedLanguages.fs index cadb8c5fe..b75bd4abd 100644 --- a/src/FsAutoComplete.Core/NestedLanguages.fs +++ b/src/FsAutoComplete.Core/NestedLanguages.fs @@ -79,7 +79,7 @@ let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option", 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 = "
" + // let binding that is a string value that has the StringSyntax attribute on it - [] let html = "
" // all of the above but with literals | _ -> None @@ -158,7 +158,7 @@ let private parametersThatAreStringSyntax match checkResults.GetSymbolUseAtLocation( endOfFinalTextToken.Line, - endOfFinalTextToken.Column - 1, + endOfFinalTextToken.Column - 1, // TODO: check off-by-one here? lineText, precedingParts |> List.map (fun i -> i.idText) ) @@ -171,7 +171,7 @@ let private parametersThatAreStringSyntax ) let sym = usage.Symbol - // todo: keep MRU map of symbols to parameters and MRU of parameters to stringsyntax status + // todo: keep MRU map of symbols to parameters and MRU of parameters to StringSyntax status match sym with | :? FSharpMemberOrFunctionOrValue as mfv -> @@ -194,6 +194,71 @@ let private parametersThatAreStringSyntax return returnVal.ToArray() } +let private hasSingleStringParameter ( + parameters: StringParameter[], + checkResults: FSharpCheckFileResults, + text: VolatileFile + ) : 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 + 1, + 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 languageName = sym.DisplayName // TODO: what about funky names? + let allParameters = mfv.CurriedParameterGroups |> Seq.collect id + let firstParameter = allParameters |> Seq.tryHead + let hasOthers = allParameters |> Seq.skip 1 |> Seq.isEmpty |> not + match hasOthers, firstParameter with + | _, None -> () + | true, _ -> () + | false, Some fsharpP -> + 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}" + ) + let baseType = fsharpP.Type.StripAbbreviations() + if baseType.BasicQualifiedName = "System.String" then + returnVal.Add + { Language = languageName + Ranges = rangeMinusRanges p.parameterRange p.rangesToRemove } + else + () + | _ -> () + + 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 @@ -214,7 +279,7 @@ let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : Nest $"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) + let! actualStringSyntaxParameters = hasSingleStringParameter (potentialParameters, tyRes.GetCheckResults, text) // || parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text) logger.info ( Log.setMessageI diff --git a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs index 3517becb3..f0e9185ae 100644 --- a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs @@ -60,7 +60,7 @@ let tests state = [| ("uri", [| (5, 31), (5, 51) |]) |] server ]) - fserverTestList "let bound function member" state defaultConfigDto None (fun server -> + serverTestList "let bound function member" state defaultConfigDto None (fun server -> [ hasLanguages "with single string parameter" """ @@ -68,4 +68,39 @@ let tests state = let u = foo "https://google.com" """ [| ("uri", [| (5, 31), (5, 51) |]) |] - server ]) ] ] + server ]) ] + ftestList "Sql" [ + serverTestList "loose function" state defaultConfigDto None (fun server -> [ + hasLanguages + "with single string parameter and string literal" + """ + let foo (s: string) = () + let u = foo "https://google.com" + """ + [| ("foo", [| (2, 24), (2, 44) |]) |] + server + + hasLanguages + "with single string parameter and interpolated string literal" + """ + let foo (s: string) = () + let u = foo $"https://{true}.com" + """ + [| ("foo", [| (2, 24), (2, 35) + (2,39), (2, 45) |]) |] + server + + hasLanguages + "multiple lanuages in the same document" + """ + let html (s: string) = () + let sql (s: string) = () + let myWebPage = html "WOWEE" + let myQuery = sql "select * from accounts where net_worth > 1000000" + """ + [| ("html", [| (3, 33), (3, 53) |]) + ("sql", [| (4, 30), (4, 80) |]) |] + server + ] + ) + ]] From af2890534db9b04b3aecd2672fd4732e223f057c Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 17:16:06 -0600 Subject: [PATCH 06/12] tests based on StringSyntax --- .vscode/launch.json | 15 +--- src/FsAutoComplete.Core/NestedLanguages.fs | 52 +++++++------ .../NestedLanguageTests.fs | 76 ++++++++++--------- 3 files changed, 70 insertions(+), 73 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e9f29fcfc..23483d6a7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,7 +52,7 @@ "args": [ "--debug", "--filter", - "FSAC.lsp.${input:loader}.${input:lsp-server}.${input:testName}" + "FSAC.lsp.${input:loader}.${input:testName}" ] } ], @@ -77,21 +77,10 @@ "default": "WorkspaceLoader", "type": "pickString" }, - - { - "id": "lsp-server", - "description": "The lsp serrver", - "options": [ - "FSharpLspServer", - "AdaptiveLspServer" - ], - "default": "AdaptiveLspServer", - "type": "pickString" - }, { "id": "testName", "description": "the name of the test as provided to `testCase`", "type": "promptString" } ] -} +} \ No newline at end of file diff --git a/src/FsAutoComplete.Core/NestedLanguages.fs b/src/FsAutoComplete.Core/NestedLanguages.fs index b75bd4abd..9555df72e 100644 --- a/src/FsAutoComplete.Core/NestedLanguages.fs +++ b/src/FsAutoComplete.Core/NestedLanguages.fs @@ -148,7 +148,7 @@ let private parametersThatAreStringSyntax $"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 precedingParts, lastPart = p.methodIdent.[0..^1], p.methodIdent[^0] + let lastPart = p.methodIdent[^0] let endOfFinalTextToken = lastPart.idRange.End match text.Source.GetLine(endOfFinalTextToken) with @@ -158,9 +158,9 @@ let private parametersThatAreStringSyntax match checkResults.GetSymbolUseAtLocation( endOfFinalTextToken.Line, - endOfFinalTextToken.Column - 1, // TODO: check off-by-one here? + endOfFinalTextToken.Column, lineText, - precedingParts |> List.map (fun i -> i.idText) + p.methodIdent |> List.map (fun x -> x.idText) ) with | None -> () @@ -194,6 +194,9 @@ let private parametersThatAreStringSyntax return returnVal.ToArray() } +let private safeNestedLanguageNames = + System.Collections.Generic.HashSet(["html"; "svg"; "css"; "sql"; "js"; "python"; "uri"; "regex"; "xml"; "json"], System.StringComparer.OrdinalIgnoreCase) + let private hasSingleStringParameter ( parameters: StringParameter[], checkResults: FSharpCheckFileResults, @@ -236,24 +239,26 @@ let private hasSingleStringParameter ( match sym with | :? FSharpMemberOrFunctionOrValue as mfv -> let languageName = sym.DisplayName // TODO: what about funky names? - let allParameters = mfv.CurriedParameterGroups |> Seq.collect id - let firstParameter = allParameters |> Seq.tryHead - let hasOthers = allParameters |> Seq.skip 1 |> Seq.isEmpty |> not - match hasOthers, firstParameter with - | _, None -> () - | true, _ -> () - | false, Some fsharpP -> - 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}" - ) - let baseType = fsharpP.Type.StripAbbreviations() - if baseType.BasicQualifiedName = "System.String" then - returnVal.Add - { Language = languageName - Ranges = rangeMinusRanges p.parameterRange p.rangesToRemove } - else - () + if safeNestedLanguageNames.Contains(languageName) + then + let allParameters = mfv.CurriedParameterGroups |> Seq.collect id + let firstParameter = allParameters |> Seq.tryHead + let hasOthers = allParameters |> Seq.skip 1 |> Seq.isEmpty |> not + match hasOthers, firstParameter with + | _, None -> () + | true, _ -> () + | false, Some fsharpP -> + 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}" + ) + let baseType = fsharpP.Type.StripAbbreviations() + if baseType.BasicQualifiedName = "System.String" then + returnVal.Add + { Language = languageName + Ranges = rangeMinusRanges p.parameterRange p.rangesToRemove } + else + () | _ -> () return returnVal.ToArray() @@ -279,8 +284,9 @@ let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : Nest $"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 = hasSingleStringParameter (potentialParameters, tyRes.GetCheckResults, text) // || parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text) - + //let! singleStringParameters = hasSingleStringParameter (potentialParameters, tyRes.GetCheckResults, text) + let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text) + //let actualStringSyntaxParameters = Array.append singleStringParameters stringSyntaxParameters logger.info ( Log.setMessageI $"Found {actualStringSyntaxParameters.Length:stringParams} actual parameters in {text.FileName:filename}@{text.Version:version}" diff --git a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs index f0e9185ae..df2025927 100644 --- a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs @@ -38,7 +38,8 @@ let tests state = "nested languages" [ testList "BCL" - [ serverTestList "class member" state defaultConfigDto None (fun server -> + // pending because class members don't return attributes in the FCS Parameter API + [ pserverTestList "class member" state defaultConfigDto None (fun server -> [ hasLanguages "with single string parameter" """ @@ -48,59 +49,60 @@ let tests state = server ]) ] testList "FSharp Code" - [ serverTestList "class member" state defaultConfigDto None (fun server -> + // pending because class members don't return attributes in the FCS Parameter API + [ pserverTestList "class member" state defaultConfigDto None (fun server -> [ hasLanguages "with single string parameter" """ type Foo() = - member x.Uri([] uriString: string) = () + member x.Boo([] uriString: string) = () let f = new Foo() - let u = f.Uri("https://google.com") + let u = f.Boo("https://google.com") """ - [| ("uri", [| (5, 31), (5, 51) |]) |] + [| ("uri", [| (4, 26), (4, 46) |]) |] server ]) serverTestList "let bound function member" state defaultConfigDto None (fun server -> [ hasLanguages "with single string parameter" """ - let foo ([] uriString: string) = () - let u = foo "https://google.com" + let boo ([] uriString: string) = () + let u = boo "https://google.com" """ - [| ("uri", [| (5, 31), (5, 51) |]) |] - server ]) ] - ftestList "Sql" [ - serverTestList "loose function" state defaultConfigDto None (fun server -> [ - hasLanguages - "with single string parameter and string literal" - """ - let foo (s: string) = () - let u = foo "https://google.com" - """ - [| ("foo", [| (2, 24), (2, 44) |]) |] + [| ("uri", [| (2, 24), (2, 44) |]) |] server - hasLanguages - "with single string parameter and interpolated string literal" - """ - let foo (s: string) = () - let u = foo $"https://{true}.com" + hasLanguages + "with single string parameter and string literal" + """ + let uri ([]s: string) = () + let u = uri "https://google.com" """ - [| ("foo", [| (2, 24), (2, 35) - (2,39), (2, 45) |]) |] - server + [| ("uri", [| (2, 24), (2, 44) |]) |] + server - hasLanguages - "multiple lanuages in the same document" - """ - let html (s: string) = () - let sql (s: string) = () - let myWebPage = html "WOWEE" + hasLanguages + "with single string parameter and interpolated string literal" + """ + let uri ([]s: string) = () + let u = uri $"https://{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", [| (3, 33), (3, 53) |]) - ("sql", [| (4, 30), (4, 80) |]) |] - server - ] + [| ("html", [| (3, 33), (3, 51) |]) + ("sql", [| (4, 30), (4, 80) |]) |] + server + ] ) - ]] + ] + ] From 9afcc2d80d62b2f06408631512b45fad7e49d89b Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 17:18:47 -0600 Subject: [PATCH 07/12] formatting --- src/FsAutoComplete.Core/NestedLanguages.fs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/FsAutoComplete.Core/NestedLanguages.fs b/src/FsAutoComplete.Core/NestedLanguages.fs index 9555df72e..581f2d064 100644 --- a/src/FsAutoComplete.Core/NestedLanguages.fs +++ b/src/FsAutoComplete.Core/NestedLanguages.fs @@ -195,9 +195,13 @@ let private parametersThatAreStringSyntax } let private safeNestedLanguageNames = - System.Collections.Generic.HashSet(["html"; "svg"; "css"; "sql"; "js"; "python"; "uri"; "regex"; "xml"; "json"], System.StringComparer.OrdinalIgnoreCase) + System.Collections.Generic.HashSet( + [ "html"; "svg"; "css"; "sql"; "js"; "python"; "uri"; "regex"; "xml"; "json" ], + System.StringComparer.OrdinalIgnoreCase + ) -let private hasSingleStringParameter ( +let private hasSingleStringParameter + ( parameters: StringParameter[], checkResults: FSharpCheckFileResults, text: VolatileFile @@ -239,20 +243,23 @@ let private hasSingleStringParameter ( match sym with | :? FSharpMemberOrFunctionOrValue as mfv -> let languageName = sym.DisplayName // TODO: what about funky names? - if safeNestedLanguageNames.Contains(languageName) - then + + if safeNestedLanguageNames.Contains(languageName) then let allParameters = mfv.CurriedParameterGroups |> Seq.collect id let firstParameter = allParameters |> Seq.tryHead let hasOthers = allParameters |> Seq.skip 1 |> Seq.isEmpty |> not + match hasOthers, firstParameter with | _, None -> () - | true, _ -> () + | true, _ -> () | false, Some fsharpP -> 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}" ) + let baseType = fsharpP.Type.StripAbbreviations() + if baseType.BasicQualifiedName = "System.String" then returnVal.Add { Language = languageName From 270e5cdc9e6d24cbc6b278fb811559b978e030c7 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 19:25:08 -0600 Subject: [PATCH 08/12] bump to .NET 8 --- global.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/global.json b/global.json index 31d071663..f3365c418 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,5 @@ { "sdk": { - "version": "7.0.400", - "allowPrerelease": true + "version": "8.0.100" } } \ No newline at end of file From fd7c31b37b84da8b2de8cedde6b1b7b2b2b4a101 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 19:30:09 -0600 Subject: [PATCH 09/12] allow .NET 8 SDKs for local use --- global.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/global.json b/global.json index f3365c418..7dd854d23 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "8.0.100" + "version": "7.0.400", + "rollForward": "major" } } \ No newline at end of file From af9030a562ecbc7327415980d5f0230a8c5c9f5d Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 19:43:39 -0600 Subject: [PATCH 10/12] try to workaround checkout issue in CI --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 24c4f1bca..40e179cb3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: name: Build on ${{matrix.os}} for ${{ matrix.label }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # setup .NET per the repo global.json - name: Setup .NET From b13a6c7cf6a27303bb7e486f6035317dba88c7da Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Fri, 29 Dec 2023 19:47:20 -0600 Subject: [PATCH 11/12] try this other way to work around it --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 40e179cb3..d545b57fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,6 +62,12 @@ jobs: steps: - uses: actions/checkout@v4 + if: ${{ github.event_name == 'pull_request' }} + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: actions/checkout@v4 + if: ${{ github.event_name == 'push' }} # setup .NET per the repo global.json - name: Setup .NET From 18e4ddab9aeb4c4d9bc07e47b34740a8df887788 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 31 Dec 2023 15:03:28 -0600 Subject: [PATCH 12/12] a lot more testing and fine tuning based on Ionide integration work --- src/FsAutoComplete.Core/NestedLanguages.fs | 237 ++++++++++-------- .../NestedLanguageTests.fs | 168 ++++++++++--- 2 files changed, 259 insertions(+), 146 deletions(-) diff --git a/src/FsAutoComplete.Core/NestedLanguages.fs b/src/FsAutoComplete.Core/NestedLanguages.fs index 581f2d064..064df7301 100644 --- a/src/FsAutoComplete.Core/NestedLanguages.fs +++ b/src/FsAutoComplete.Core/NestedLanguages.fs @@ -14,15 +14,71 @@ let logger = LogProvider.getLoggerByName "NestedLanguages" type private StringParameter = { methodIdent: LongIdent parameterRange: Range - rangesToRemove: Range[] + rangesToRemove: Range array parameterPosition: int } -let discoverRangesToRemoveForInterpolatedString (list: SynInterpolatedStringPart list) = - list - |> List.choose (function - | SynInterpolatedStringPart.FillExpr(fillExpr = e) -> Some e.Range - | _ -> None) - |> List.toArray + +/// for virtual documents based on interpolated strings we need to remove two kinds of trivia from the overall string portions. +/// * 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. +/// * for string literals, we need to remove the prefix/suffix tokens (quotes, interpolation brackets, format specifiers, etc) so that the only content visible +/// to the virtual document is the actual string content. +/// +/// 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 +/// interpolation start/end tokens. +let private discoverRangesToRemoveForInterpolatedString + (stringKind: SynStringKind) + (parts: SynInterpolatedStringPart[]) + = + parts + |> Array.indexed + |> Array.collect (fun (index, part) -> + match part with + | SynInterpolatedStringPart.FillExpr(fillExpr = e) -> [| e.Range |] + // 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 + | SynInterpolatedStringPart.String(range = range) when index = 0 -> + [| + // leading tokens adjustment + // GAP: we don't know how many interpolation $ or " there are, so we are guessing + match stringKind with + | SynStringKind.Regular -> + // 'regular' means $" leading identifier + range.WithEnd(range.Start.WithColumn(range.StartColumn + 2)) + | SynStringKind.TripleQuote -> + // 'triple quote' means $""" leading identifier + range.WithEnd(range.Start.WithColumn(range.StartColumn + 4)) + // there's no such thing as a verbatim interpolated string + | SynStringKind.Verbatim -> () + + // trailing token adjustment- only an opening bracket { + // GAP: this is the feature gap - we don't know about format specifiers + range.WithStart(range.End.WithColumn(range.EndColumn - 1)) + + |] + // for the last part we have a single-character interpolation bracket on the left and the 'trailing' string elements on the right + | SynInterpolatedStringPart.String(range = range) when index = parts.Length - 1 -> + [| + // leading token adjustment - only a closing bracket } + range.WithEnd(range.Start.WithColumn(range.StartColumn + 1)) + + // trailing tokens adjustment + // GAP: we don't know how many """ to adjust for triple-quote interpolated string endings + match stringKind with + | SynStringKind.Regular -> + // 'regular' means trailing identifier " + range.WithStart(range.End.WithColumn(range.EndColumn - 1)) + | SynStringKind.TripleQuote -> + // 'triple quote' means trailing identifier """ + range.WithStart(range.End.WithColumn(range.EndColumn - 3)) + // no such thing as verbatim interpolated strings + | SynStringKind.Verbatim -> () |] + // 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 + | SynInterpolatedStringPart.String(range = range) -> + [| + // leading token adjustment - only a closing bracket } + range.WithEnd(range.Start.WithColumn(range.StartColumn + 1)) + // trailing token adjustment- only an opening bracket { + // GAP: this is the feature gap - we don't know about format specifiers here + range.WithStart(range.End.WithColumn(range.EndColumn - 1)) |]) let private (|Ident|_|) (e: SynExpr) = match e with @@ -30,7 +86,24 @@ let private (|Ident|_|) (e: SynExpr) = | SynExpr.LongIdent(longDotId = SynLongIdent(id = ident)) -> Some ident | _ -> None -let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option = +/// in order for nested documents to be recognized as their document types, the string quotes (and other tokens) need to be removed +/// from the actual string content. +let private removeStringTokensFromStringRange (kind: SynStringKind) (range: Range) : Range array = + match kind with + | SynStringKind.Regular -> + // we need to trim the double-quote off of the start and end + [| Range.mkRange range.FileName range.Start (range.Start.WithColumn(range.StartColumn + 1)) + Range.mkRange range.FileName (range.End.WithColumn(range.EndColumn - 1)) range.End |] + | SynStringKind.Verbatim -> + // we need to trim the @+double-quote off of the start and double-quote off the end + [| Range.mkRange range.FileName range.Start (range.Start.WithColumn(range.StartColumn + 2)) + Range.mkRange range.FileName (range.End.WithColumn(range.EndColumn - 1)) range.End |] + | SynStringKind.TripleQuote -> + // we need to trim the @+double-quote off of the start and double-quote off the end + [| Range.mkRange range.FileName range.Start (range.Start.WithColumn(range.StartColumn + 2)) + Range.mkRange range.FileName (range.End.WithColumn(range.EndColumn - 1)) range.End |] + +let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : StringParameter array option = match e with // lines inside a binding // let doThing () = @@ -39,34 +112,46 @@ let rec private (|IsApplicationWithStringParameters|_|) (e: SynExpr) : option" |> c.M // $"
{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 + 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), _))) + 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), _)) -> + | SynExpr.App(funcExpr = Ident(ident); argExpr = SynExpr.Const(SynConst.String(_text, kind, range), _)) -> Some( [| { methodIdent = ident parameterRange = range - rangesToRemove = [||] + 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; range = range))) + 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; range = range)) -> - let rangesToRemove = discoverRangesToRemoveForInterpolatedString parts + | SynExpr.App( + funcExpr = Ident(ident) + argExpr = SynExpr.InterpolatedString(contents = parts; synStringKind = stringKind; range = range)) -> + let rangesToRemove = + discoverRangesToRemoveForInterpolatedString stringKind (Array.ofList parts) Some( [| { methodIdent = ident @@ -117,9 +202,11 @@ let private (|IsStringSyntax|_|) (a: FSharpAttribute) = | _ -> None | _ -> None -type NestedLanguageDocument = { Language: string; Ranges: Range[] } +type NestedLanguageDocument = + { Language: string + Ranges: Range array } -let rangeMinusRanges (totalRange: Range) (rangesToRemove: Range[]) : Range[] = +let rangeMinusRanges (totalRange: Range) (rangesToRemove: Range array) : Range array = match rangesToRemove with | [||] -> [| totalRange |] | _ -> @@ -127,18 +214,25 @@ let rangeMinusRanges (totalRange: Range) (rangesToRemove: Range[]) : Range[] = let mutable currentStart = totalRange.Start for r in rangesToRemove do - returnVal.Add(Range.mkRange totalRange.FileName currentStart r.Start) - currentStart <- r.End + 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.Add(Range.mkRange totalRange.FileName currentStart totalRange.End) returnVal.ToArray() let private parametersThatAreStringSyntax ( - parameters: StringParameter[], + parameters: StringParameter array, checkResults: FSharpCheckFileResults, text: VolatileFile - ) : Async = + ) : NestedLanguageDocument array Async = async { let returnVal = ResizeArray() @@ -194,90 +288,12 @@ let private parametersThatAreStringSyntax return returnVal.ToArray() } -let private safeNestedLanguageNames = - System.Collections.Generic.HashSet( - [ "html"; "svg"; "css"; "sql"; "js"; "python"; "uri"; "regex"; "xml"; "json" ], - System.StringComparer.OrdinalIgnoreCase - ) - -let private hasSingleStringParameter - ( - parameters: StringParameter[], - checkResults: FSharpCheckFileResults, - text: VolatileFile - ) : 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 + 1, - 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 languageName = sym.DisplayName // TODO: what about funky names? - - if safeNestedLanguageNames.Contains(languageName) then - let allParameters = mfv.CurriedParameterGroups |> Seq.collect id - let firstParameter = allParameters |> Seq.tryHead - let hasOthers = allParameters |> Seq.skip 1 |> Seq.isEmpty |> not - - match hasOthers, firstParameter with - | _, None -> () - | true, _ -> () - | false, Some fsharpP -> - 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}" - ) - - let baseType = fsharpP.Type.StripAbbreviations() - - if baseType.BasicQualifiedName = "System.String" then - returnVal.Add - { Language = languageName - Ranges = rangeMinusRanges p.parameterRange p.rangesToRemove } - else - () - | _ -> () - - 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[] Async = +let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : NestedLanguageDocument array Async = async { - // get all string constants let potentialParameters = findParametersForParseTree tyRes.GetAST logger.info ( @@ -291,9 +307,8 @@ let findNestedLanguages (tyRes: ParseAndCheckResults, text: VolatileFile) : Nest $"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! singleStringParameters = hasSingleStringParameter (potentialParameters, tyRes.GetCheckResults, text) let! actualStringSyntaxParameters = parametersThatAreStringSyntax (potentialParameters, tyRes.GetCheckResults, text) - //let actualStringSyntaxParameters = Array.append singleStringParameters stringSyntaxParameters + logger.info ( Log.setMessageI $"Found {actualStringSyntaxParameters.Length:stringParams} actual parameters in {text.FileName:filename}@{text.Version:version}" diff --git a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs index df2025927..42ba6afae 100644 --- a/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs +++ b/test/FsAutoComplete.Tests.Lsp/NestedLanguageTests.fs @@ -14,6 +14,56 @@ type Document with |> 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.[r.Start.Line] + let endLine = lines.[r.End.Line] + + if r.Start.Line = r.End.Line then + startLine.Substring(r.Start.Character, r.End.Character - r.Start.Character) + else + let start = startLine.Substring(r.Start.Character) + let ``end`` = endLine.Substring(0, r.End.Character) + + let middle = + lines.[r.Start.Line + 1 .. r.End.Line - 1] |> 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 @@ -30,29 +80,29 @@ let hasLanguages name source expectedLanguages server = { Start = { Line = sl; Character = sc } End = { Line = el; Character = ec } }) }) - Expect.equal nestedLanguages.NestedLanguages mappedExpectedLanguages "languages" + Expect.equal + nestedLanguages.NestedLanguages + mappedExpectedLanguages + (contentErrorMessage nestedLanguages.NestedLanguages mappedExpectedLanguages source) } let tests state = testList "nested languages" - [ testList - "BCL" + [ ptestList + "unsupported scenarios" // pending because class members don't return attributes in the FCS Parameter API - [ pserverTestList "class member" state defaultConfigDto None (fun server -> + [ serverTestList "class member" state defaultConfigDto None (fun server -> [ hasLanguages - "with single string parameter" + "BCL type" """ let b = System.UriBuilder("https://google.com") """ [| ("uri", [| (1, 38), (1, 58) |]) |] - server ]) ] - testList - "FSharp Code" - // pending because class members don't return attributes in the FCS Parameter API - [ pserverTestList "class member" state defaultConfigDto None (fun server -> - [ hasLanguages - "with single string parameter" + server + + hasLanguages + "F#-defined type" """ type Foo() = member x.Boo([] uriString: string) = () @@ -61,48 +111,96 @@ let tests state = """ [| ("uri", [| (4, 26), (4, 46) |]) |] 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", [| (2, 26), (2, 34); (2, 42), (2, 46) |]) |] + server + - serverTestList "let bound function member" state defaultConfigDto None (fun 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 - "with single string parameter" + "normal string value" """ let boo ([] uriString: string) = () let u = boo "https://google.com" """ - [| ("uri", [| (2, 24), (2, 44) |]) |] + // note for reader - 24 is the start quote, 44 is the end quote, so we want a doc including 25-43 + [| ("uri", [| (2, 25), (2, 43) |]) |] server hasLanguages - "with single string parameter and string literal" - """ - let uri ([]s: string) = () - let u = uri "https://google.com" + "verbatim string value" + """ + let boo ([] uriString: string) = () + let u = boo @"https://google.com" + """ + [| ("uri", [| (2, 26), (2, 44) |]) |] + server + + hasLanguages + "triple-quote string value" + """ + let boo ([] uriString: string) = () + let u = boo "https://google.com" """ - [| ("uri", [| (2, 24), (2, 44) |]) |] - server + [| ("uri", [| (2, 25), (2, 43) |]) |] + server hasLanguages - "with single string parameter and interpolated string literal" - """ + "simple interpolated string" + """ let uri ([]s: string) = () let u = uri $"https://{true}.com" """ - [| ("uri", [| (2, 24), (2, 35) - (2,39), (2, 45) |]) |] - server + [| ("uri", [| (2, 26), (2, 34); (2, 40), (2, 44) |]) |] + 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" - """ + "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", [| (3, 33), (3, 51) |]) - ("sql", [| (4, 30), (4, 80) |]) |] - server - ] - ) - ] - ] + [| ("html", [| (3, 34), (3, 50) |]); ("sql", [| (4, 31), (4, 79) |]) |] + server ]) ] ]