diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index f8bbf03..c253063 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -4,6 +4,11 @@ on: pull_request: branches: - main + types: + - opened + - reopened + - synchronize + - ready_for_review permissions: contents: read diff --git a/.github/workflows/on-push-tag.yml b/.github/workflows/on-push-tag.yml index 6eed98a..5c8b231 100644 --- a/.github/workflows/on-push-tag.yml +++ b/.github/workflows/on-push-tag.yml @@ -45,7 +45,7 @@ jobs: shell: bash run: | rm -rf vscode-fscript/server - dotnet publish src/FScript.LanguageServer/FScript.LanguageServer.fsproj -c Release -p:PublishAot=false -o vscode-fscript/server + dotnet publish src/FScript.LanguageServer/FScript.LanguageServer.csproj -c Release -p:PublishAot=false -o vscode-fscript/server - name: Install extension packaging tool run: npm install -g @vscode/vsce@3.5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 35c2dba..88ae809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,19 @@ All notable changes to FScript are documented in this file. ## [Unreleased] +- Removed F# sources from `src/FScript.LanguageServer*` by moving LSP semantic modules into `FScript.CSharpInterop` and keeping `FScript.LanguageServer` as C# host. +- Replaced `FScript.LanguageServer.Tests` project with a C# test project and C# LSP test harness to remove F# compile cost from LanguageServer test builds. +- Deleted obsolete F# LanguageServer test sources after C# test project migration. +- Renamed `FScript.CSharpInterop/LanguageServerLegacy` to `FScript.CSharpInterop/LanguageServer` to reflect the new primary architecture. +- CI now runs branch update builds on PR `synchronize` events while keeping `ci-main` scoped to `main` pushes to avoid duplicate runs. - Enabled F# preview parallel compilation globally, disabled deterministic builds, and removed global RuntimeIdentifiers to reduce CI build latency. +- Added `FScript.CSharpInterop` as a stable bridge for parse/infer/runtime-extern/stdlib-source services and wired LanguageServer through it. +- Added `FScript.LanguageServer` host executable as the migration entrypoint for C#-owned LSP startup. +- Added a first native C# LSP server core (JSON-RPC transport, initialize/shutdown, text sync, and stdlib-source request) with dedicated integration tests. +- Extended the native C# LSP core with diagnostics publishing and `viewAst`/`viewInferredAst` command handling. +- Switched C# LSP host to full-method dispatch parity via shared handlers, made it the default test target, and updated extension/tag packaging to use `FScript.LanguageServer.dll`. +- Replaced the F# LSP server executable with `FScript.LanguageServer` (C#) and moved F# LSP logic into `FScript.LanguageServer.Core`. +- Fixed imported qualified type annotations (for example `common.ProjectInfo`) in parser/type inference to prevent false type mismatches. ## [0.33.0] diff --git a/FScript.sln b/FScript.sln index 6eb4b98..c6937ec 100644 --- a/FScript.sln +++ b/FScript.sln @@ -17,9 +17,11 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.Runtime", "src\FScr EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.Runtime.Tests", "tests\FScript.Runtime.Tests\FScript.Runtime.Tests.fsproj", "{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.LanguageServer", "src\FScript.LanguageServer\FScript.LanguageServer.fsproj", "{E22A34B5-F5E8-422D-9BA5-932B3C45188F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FScript.LanguageServer.Tests", "tests\FScript.LanguageServer.Tests\FScript.LanguageServer.Tests.csproj", "{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}" EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.LanguageServer.Tests", "tests\FScript.LanguageServer.Tests\FScript.LanguageServer.Tests.fsproj", "{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}" +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.CSharpInterop", "src\FScript.CSharpInterop\FScript.CSharpInterop.fsproj", "{8A28B784-F90B-469C-91BE-F96F63ACEA32}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FScript.LanguageServer", "src\FScript.LanguageServer\FScript.LanguageServer.csproj", "{57518676-01F0-4D5B-A53B-7A06DBA9AA04}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -91,18 +93,6 @@ Global {1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x64.Build.0 = Release|Any CPU {1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x86.ActiveCfg = Release|Any CPU {1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x86.Build.0 = Release|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x64.ActiveCfg = Debug|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x64.Build.0 = Debug|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x86.ActiveCfg = Debug|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x86.Build.0 = Debug|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|Any CPU.Build.0 = Release|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x64.ActiveCfg = Release|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x64.Build.0 = Release|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x86.ActiveCfg = Release|Any CPU - {E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x86.Build.0 = Release|Any CPU {B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -115,6 +105,30 @@ Global {B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x64.Build.0 = Release|Any CPU {B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x86.ActiveCfg = Release|Any CPU {B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x86.Build.0 = Release|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x64.Build.0 = Debug|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x86.Build.0 = Debug|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|Any CPU.Build.0 = Release|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x64.ActiveCfg = Release|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x64.Build.0 = Release|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x86.ActiveCfg = Release|Any CPU + {8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x86.Build.0 = Release|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x64.ActiveCfg = Debug|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x64.Build.0 = Debug|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x86.ActiveCfg = Debug|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x86.Build.0 = Debug|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|Any CPU.Build.0 = Release|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x64.ActiveCfg = Release|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x64.Build.0 = Release|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x86.ActiveCfg = Release|Any CPU + {57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -125,7 +139,8 @@ Global {9C62883E-EFB0-4D9E-84F3-4138C123F55E} = {0AB3BF05-4346-4AA6-1389-037BE0695223} {8C2C5767-857A-44B0-80C2-DC90E0A60F4D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {1E2C7B34-04B8-42C9-880D-CC47DEC156A7} = {0AB3BF05-4346-4AA6-1389-037BE0695223} - {E22A34B5-F5E8-422D-9BA5-932B3C45188F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {B734E1E1-59C2-47E0-8D19-A9C5C95938F1} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {8A28B784-F90B-469C-91BE-F96F63ACEA32} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {57518676-01F0-4D5B-A53B-7A06DBA9AA04} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/docs/architecture/assemblies-and-roles.md b/docs/architecture/assemblies-and-roles.md index 76565a4..d497f61 100644 --- a/docs/architecture/assemblies-and-roles.md +++ b/docs/architecture/assemblies-and-roles.md @@ -55,6 +55,32 @@ Use this when: NuGet: - `MagnusOpera.FScript.Runtime` +### `FScript.CSharpInterop` +Role: +- C#-friendly integration facade over language + runtime services. + +Responsibilities: +- Resolve runtime extern catalog from source path/root context. +- Parse with include/import expansion through a stable interop entry point. +- Run inference APIs through a single host-facing surface. +- Expose stdlib virtual source loading for editor integrations. + +Use this when: +- You integrate FScript from C# and want to avoid direct F# compiler/runtime internals. +- You build tooling services (for example LSP hosts) with a stable boundary. + +### `FScript.LanguageServer` +Role: +- C# host executable for the Language Server process. + +Responsibilities: +- Provide the production C# process host for LSP startup/dispatch. +- Execute the full LSP method surface used by the VS Code extension. +- Keep protocol behavior aligned with existing language/runtime analysis services. + +Use this when: +- You want C# ownership of the server host process while reusing existing language services. + ## Typical composition ### CLI execution path @@ -72,6 +98,8 @@ NuGet: ## Dependency direction - `FScript.Language` has no dependency on `FScript.Runtime`. - `FScript.Runtime` depends on `FScript.Language` types. +- `FScript.CSharpInterop` depends on both `FScript.Language` and `FScript.Runtime`. +- `FScript.LanguageServer` depends on `FScript.CSharpInterop`. - `FScript` depends on both `FScript.Language` and `FScript.Runtime`. This keeps the language engine reusable while runtime capabilities remain host-configurable. diff --git a/src/FScript.CSharpInterop/FScript.CSharpInterop.fsproj b/src/FScript.CSharpInterop/FScript.CSharpInterop.fsproj new file mode 100644 index 0000000..2b6bf1c --- /dev/null +++ b/src/FScript.CSharpInterop/FScript.CSharpInterop.fsproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + + + + + + + + + + + + + + + + + + diff --git a/src/FScript.CSharpInterop/InteropServices.fs b/src/FScript.CSharpInterop/InteropServices.fs new file mode 100644 index 0000000..22a1979 --- /dev/null +++ b/src/FScript.CSharpInterop/InteropServices.fs @@ -0,0 +1,63 @@ +namespace FScript.CSharpInterop + +open System +open System.IO +open FScript.Language +open FScript.Runtime + +module InteropServices = + let private resolveRootDirectory (sourcePath: string) = + try + match Path.GetDirectoryName(sourcePath) with + | null + | "" -> Directory.GetCurrentDirectory() + | dir -> dir + with _ -> + Directory.GetCurrentDirectory() + + let runtimeExternsForSourcePath (sourcePath: string) : ExternalFunction list = + let ctx = { HostContext.RootDirectory = resolveRootDirectory sourcePath } + Registry.all ctx + + let parseProgramFromSourceWithIncludes (sourcePath: string) (sourceText: string) : Program = + let rootDirectory = resolveRootDirectory sourcePath + IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText + + let inferProgramWithExterns (externs: ExternalFunction list) (program: Program) : TypeInfer.TypedProgram = + TypeInfer.inferProgramWithExterns externs program + + let inferProgramWithExternsAndLocalVariableTypes (externs: ExternalFunction list) (program: Program) : TypeInfer.TypedProgram * TypeInfer.LocalVariableTypeInfo list = + TypeInfer.inferProgramWithExternsAndLocalVariableTypes externs program + + let inferStdlibWithExternsRaw (externs: ExternalFunction list) : TypeInfer.TypedProgram = + TypeInfer.inferProgramWithExternsRaw externs (Stdlib.loadProgram()) + + let stdlibProgram () : Program = + Stdlib.loadProgram() + + let tryLoadStdlibSourceText (uri: string) : string option = + try + let parsed = Uri(uri) + if not (String.Equals(parsed.Scheme, "fscript-stdlib", StringComparison.OrdinalIgnoreCase)) then + None + else + let fileName = parsed.AbsolutePath.TrimStart('/') + let resourceName = + match fileName with + | "Option.fss" -> Some "FScript.Language.Stdlib.Option.fss" + | "List.fss" -> Some "FScript.Language.Stdlib.List.fss" + | "Map.fss" -> Some "FScript.Language.Stdlib.Map.fss" + | _ -> None + + match resourceName with + | None -> None + | Some name -> + let assembly = typeof.Assembly + match assembly.GetManifestResourceStream(name) with + | null -> None + | stream -> + use stream = stream + use reader = new StreamReader(stream) + Some(reader.ReadToEnd()) + with _ -> + None diff --git a/src/FScript.LanguageServer/AstJson.fs b/src/FScript.CSharpInterop/LanguageServer/AstJson.fs similarity index 100% rename from src/FScript.LanguageServer/AstJson.fs rename to src/FScript.CSharpInterop/LanguageServer/AstJson.fs diff --git a/src/FScript.LanguageServer/LspHandlers.fs b/src/FScript.CSharpInterop/LanguageServer/LspHandlers.fs similarity index 96% rename from src/FScript.LanguageServer/LspHandlers.fs rename to src/FScript.CSharpInterop/LanguageServer/LspHandlers.fs index 659c699..9b7d391 100644 --- a/src/FScript.LanguageServer/LspHandlers.fs +++ b/src/FScript.CSharpInterop/LanguageServer/LspHandlers.fs @@ -4,6 +4,7 @@ open System open System.IO open System.Text.Json.Nodes open FScript.Language +open FScript.CSharpInterop module LspHandlers = open LspModel @@ -497,12 +498,7 @@ module LspHandlers = | None -> sendCommandError idNode "internal" $"Unable to read source file '{sourcePath}'." | Some sourceText -> - let rootDirectory = - match Path.GetDirectoryName(sourcePath) with - | null - | "" -> "." - | dir -> dir - let program = IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText + let program = InteropServices.parseProgramFromSourceWithIncludes sourcePath sourceText let response = JsonObject() response["ok"] <- JsonValue.Create(true) response["data"] <- AstJson.programToJson sourcePath program @@ -528,14 +524,9 @@ module LspHandlers = | None -> sendCommandError idNode "internal" $"Unable to read source file '{sourcePath}'." | Some sourceText -> - let rootDirectory = - match Path.GetDirectoryName(sourcePath) with - | null - | "" -> "." - | dir -> dir - let program = IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText + let program = InteropServices.parseProgramFromSourceWithIncludes sourcePath sourceText let runtimeExterns = LspRuntimeExterns.forSourcePath sourcePath - let typedProgram = TypeInfer.inferProgramWithExterns runtimeExterns program + let typedProgram = InteropServices.inferProgramWithExterns runtimeExterns program let response = JsonObject() response["ok"] <- JsonValue.Create(true) response["data"] <- AstJson.typedProgramToJson sourcePath typedProgram @@ -1103,40 +1094,12 @@ module LspHandlers = | _ -> LspProtocol.sendResponse idNode None - let private tryLoadStdlibSourceText (uri: string) = - try - let parsed = Uri(uri) - if not (String.Equals(parsed.Scheme, "fscript-stdlib", StringComparison.OrdinalIgnoreCase)) then - None - else - let fileName = parsed.AbsolutePath.TrimStart('/') - let resourceName = - match fileName with - | "Option.fss" -> Some "FScript.Language.Stdlib.Option.fss" - | "List.fss" -> Some "FScript.Language.Stdlib.List.fss" - | "Map.fss" -> Some "FScript.Language.Stdlib.Map.fss" - | _ -> None - - match resourceName with - | None -> None - | Some name -> - let assembly = typeof.Assembly - match assembly.GetManifestResourceStream(name) with - | null -> - None - | stream -> - use stream = stream - use reader = new StreamReader(stream) - Some (reader.ReadToEnd()) - with _ -> - None - let handleStdlibSource (idNode: JsonNode) (paramsObj: JsonObject) = match tryGetString paramsObj "uri" with | None -> sendCommandError idNode "internal" "Missing stdlib URI." | Some uri -> - match tryLoadStdlibSourceText uri with + match InteropServices.tryLoadStdlibSourceText uri with | Some sourceText -> let response = JsonObject() response["ok"] <- JsonValue.Create(true) diff --git a/src/FScript.LanguageServer/LspModel.fs b/src/FScript.CSharpInterop/LanguageServer/LspModel.fs similarity index 100% rename from src/FScript.LanguageServer/LspModel.fs rename to src/FScript.CSharpInterop/LanguageServer/LspModel.fs diff --git a/src/FScript.LanguageServer/LspProtocol.fs b/src/FScript.CSharpInterop/LanguageServer/LspProtocol.fs similarity index 100% rename from src/FScript.LanguageServer/LspProtocol.fs rename to src/FScript.CSharpInterop/LanguageServer/LspProtocol.fs diff --git a/src/FScript.CSharpInterop/LanguageServer/LspRuntimeExterns.fs b/src/FScript.CSharpInterop/LanguageServer/LspRuntimeExterns.fs new file mode 100644 index 0000000..93b4758 --- /dev/null +++ b/src/FScript.CSharpInterop/LanguageServer/LspRuntimeExterns.fs @@ -0,0 +1,8 @@ +namespace FScript.LanguageServer + +open FScript.Language +open FScript.CSharpInterop + +module LspRuntimeExterns = + let forSourcePath (sourcePath: string) : ExternalFunction list = + InteropServices.runtimeExternsForSourcePath sourcePath diff --git a/src/FScript.LanguageServer/LspSymbols.fs b/src/FScript.CSharpInterop/LanguageServer/LspSymbols.fs similarity index 99% rename from src/FScript.LanguageServer/LspSymbols.fs rename to src/FScript.CSharpInterop/LanguageServer/LspSymbols.fs index 147a59b..2968eee 100644 --- a/src/FScript.LanguageServer/LspSymbols.fs +++ b/src/FScript.CSharpInterop/LanguageServer/LspSymbols.fs @@ -5,6 +5,7 @@ open System.Collections.Generic open System.IO open System.Text.Json.Nodes open FScript.Language +open FScript.CSharpInterop module LspSymbols = open LspModel @@ -69,7 +70,7 @@ module LspSymbols = let private stdlibFunctionSignatures : Lazy> = lazy - let typedStdlib = TypeInfer.inferProgramWithExternsRaw [] (Stdlib.loadProgram()) + let typedStdlib = InteropServices.inferStdlibWithExternsRaw [] typedStdlib |> List.collect (function | TypeInfer.TSLet(name, _, t, _, _, _) -> @@ -94,7 +95,7 @@ module LspSymbols = let private stdlibFunctionParameterNames : Lazy> = lazy - Stdlib.loadProgram() + InteropServices.stdlibProgram() |> List.collect (function | SLet(name, args, _, _, _, _) -> [ name, (args |> List.map (fun p -> p.Name)) ] @@ -106,7 +107,7 @@ module LspSymbols = let private stdlibFunctionDefinitions : Lazy> = lazy - Stdlib.loadProgram() + InteropServices.stdlibProgram() |> List.collect (function | SLet(name, _, _, _, _, span) -> match tryStdlibVirtualUriFromSource span.Start.File with @@ -355,7 +356,7 @@ module LspSymbols = let tryInferWithCurrent (candidate: Program) = try - let typed, _ = TypeInfer.inferProgramWithExternsAndLocalVariableTypes externs candidate + let typed, _ = InteropServices.inferProgramWithExternsAndLocalVariableTypes externs candidate Some typed with | _ -> None @@ -408,7 +409,7 @@ module LspSymbols = let tryInferWithCurrent (candidate: Program) = try - let _, localTypes = TypeInfer.inferProgramWithExternsAndLocalVariableTypes externs candidate + let _, localTypes = InteropServices.inferProgramWithExternsAndLocalVariableTypes externs candidate Some localTypes with | _ -> None @@ -1441,12 +1442,7 @@ module LspSymbols = try let program = if uri.StartsWith("file://", StringComparison.OrdinalIgnoreCase) then - let directory = - match System.IO.Path.GetDirectoryName(sourceName) with - | null - | "" -> "." - | dir -> dir - IncludeResolver.parseProgramFromSourceWithIncludes directory sourceName text + InteropServices.parseProgramFromSourceWithIncludes sourceName text else FScript.parseWithSourceName (Some sourceName) text parsedProgram <- Some program @@ -1463,7 +1459,7 @@ module LspSymbols = callArgumentHints <- buildCallArgumentHints program functionParameters localBindings <- buildLocalBindings program try - let typed, localTypes = TypeInfer.inferProgramWithExternsAndLocalVariableTypes runtimeExterns program + let typed, localTypes = InteropServices.inferProgramWithExternsAndLocalVariableTypes runtimeExterns program symbols <- buildSymbolsFromProgram program (Some typed) parameterTypeHints <- buildParameterTypeHints program (Some typed) functionReturnTypeHints <- buildFunctionReturnTypeHints program (Some typed) diff --git a/src/FScript.Language/Parser.fs b/src/FScript.Language/Parser.fs index c9977c4..7c51146 100644 --- a/src/FScript.Language/Parser.fs +++ b/src/FScript.Language/Parser.fs @@ -183,12 +183,29 @@ module Parser = let mutable allowIndentedApplication = true let mutable allowBinaryNewlineSkipping = true + let parseQualifiedTypeName () : string = + let first = stream.ExpectIdent("Expected type name") + let firstName = + match first.Kind with + | Ident n -> n + | _ -> "" + let parts = ResizeArray() + parts.Add(firstName) + let mutable keepGoing = true + while keepGoing && stream.Match(Dot) do + let next = stream.ExpectIdent("Expected identifier after '.' in qualified type name") + match next.Kind with + | Ident n -> parts.Add(n) + | _ -> () + keepGoing <- true + String.concat "." parts + let rec parseTypeRefAtom () : TypeRef = stream.SkipNewlines() match stream.Peek().Kind with - | Ident name -> - stream.Next() |> ignore - TRName name + | Ident _ -> + let qualified = parseQualifiedTypeName() + TRName qualified | LBrace -> stream.Next() |> ignore let isStructural = stream.Match(Bar) diff --git a/src/FScript.Language/TypeInfer.fs b/src/FScript.Language/TypeInfer.fs index ce4ff8d..86f89e2 100644 --- a/src/FScript.Language/TypeInfer.fs +++ b/src/FScript.Language/TypeInfer.fs @@ -269,6 +269,26 @@ module TypeInfer = let names = String.concat ", " many raise (TypeException { Message = $"Ambiguous declared record type for fields {shapeText}: {names}"; Span = span }) + let private resolveReferencedTypeName (knownNames: seq) (name: string) (span: Span) : string = + let nameSet = knownNames |> Set.ofSeq + if nameSet.Contains(name) then + name + elif name.Contains(".") then + let shortName = name.Split('.') |> Array.last + let matches = + knownNames + |> Seq.filter (fun candidate -> candidate = shortName || candidate.EndsWith("." + shortName, System.StringComparison.Ordinal)) + |> Seq.distinct + |> Seq.toList + match matches with + | [ single ] -> single + | [] -> name + | many -> + let options = String.concat ", " many + raise (TypeException { Message = $"Ambiguous type reference '{name}'. Candidates: {options}"; Span = span }) + else + name + let rec private annotationTypeFromRef (typeDefs: Map) (span: Span) (tref: TypeRef) : Type = match tref with | TRName "unit" -> TUnit @@ -276,7 +296,9 @@ module TypeInfer = | TRName "float" -> TFloat | TRName "bool" -> TBool | TRName "string" -> TString - | TRName name -> TNamed name + | TRName name -> + let resolvedName = resolveReferencedTypeName typeDefs.Keys name span + TNamed resolvedName | TRTuple ts -> ts |> List.map (annotationTypeFromRef typeDefs span) |> TTuple | TRFun (a, b) -> TFun(annotationTypeFromRef typeDefs span a, annotationTypeFromRef typeDefs span b) | TRPostfix (inner, "list") -> TList (annotationTypeFromRef typeDefs span inner) @@ -302,25 +324,26 @@ module TypeInfer = match builtinType name with | Some t -> t | None -> - match decls.TryFind name with + let resolvedName = resolveReferencedTypeName decls.Keys name unknownSpan + match decls.TryFind resolvedName with | Some def -> - match stack |> List.tryFindIndex ((=) name) with + match stack |> List.tryFindIndex ((=) resolvedName) with | Some 0 -> if def.IsRecursive then - TNamed name + TNamed resolvedName else - raise (TypeException { Message = $"Recursive type '{name}' requires 'type rec'"; Span = unknownSpan }) + raise (TypeException { Message = $"Recursive type '{resolvedName}' requires 'type rec'"; Span = unknownSpan }) | Some _ -> raise (TypeException { Message = "Mutual recursive types are not supported"; Span = unknownSpan }) | None -> if not def.Cases.IsEmpty then - TNamed name + TNamed resolvedName else def.Fields - |> List.map (fun (field, t) -> field, typeFromRef decls (name :: stack) t) + |> List.map (fun (field, t) -> field, typeFromRef decls (resolvedName :: stack) t) |> Map.ofList |> TRecord - | None -> TNamed name + | None -> TNamed resolvedName | TRTuple ts -> ts |> List.map (typeFromRef decls stack) |> TTuple | TRFun (a, b) -> diff --git a/src/FScript.LanguageServer/FScript.LanguageServer.csproj b/src/FScript.LanguageServer/FScript.LanguageServer.csproj new file mode 100644 index 0000000..25b10a8 --- /dev/null +++ b/src/FScript.LanguageServer/FScript.LanguageServer.csproj @@ -0,0 +1,12 @@ + + + Exe + net10.0 + enable + enable + + + + + + diff --git a/src/FScript.LanguageServer/FScript.LanguageServer.fsproj b/src/FScript.LanguageServer/FScript.LanguageServer.fsproj deleted file mode 100644 index 53ca154..0000000 --- a/src/FScript.LanguageServer/FScript.LanguageServer.fsproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - Exe - net10.0 - enable - false - - - - - - - - - - - - - - - - - - - diff --git a/src/FScript.LanguageServer/JsonRpcWire.cs b/src/FScript.LanguageServer/JsonRpcWire.cs new file mode 100644 index 0000000..649eadb --- /dev/null +++ b/src/FScript.LanguageServer/JsonRpcWire.cs @@ -0,0 +1,85 @@ +using System.Text; + +namespace FScript.LanguageServer.CSharp; + +internal static class JsonRpcWire +{ + internal static string? ReadMessage(Stream input) + { + var contentLength = -1; + + while (true) + { + var line = ReadHeaderLine(input); + if (line is null) + { + return null; + } + + if (line.Length == 0) + { + break; + } + + const string prefix = "Content-Length:"; + if (line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + var value = line[prefix.Length..].Trim(); + if (int.TryParse(value, out var parsed)) + { + contentLength = parsed; + } + } + } + + if (contentLength < 0) + { + return null; + } + + var payload = new byte[contentLength]; + var offset = 0; + while (offset < payload.Length) + { + var read = input.Read(payload, offset, payload.Length - offset); + if (read <= 0) + { + return null; + } + + offset += read; + } + + return Encoding.UTF8.GetString(payload); + } + + internal static void WriteMessage(Stream output, string json) + { + var bytes = Encoding.UTF8.GetBytes(json); + var header = Encoding.ASCII.GetBytes($"Content-Length: {bytes.Length}\r\n\r\n"); + output.Write(header, 0, header.Length); + output.Write(bytes, 0, bytes.Length); + output.Flush(); + } + + private static string? ReadHeaderLine(Stream input) + { + using var buffer = new MemoryStream(); + + while (true) + { + var value = input.ReadByte(); + if (value < 0) + { + return buffer.Length == 0 ? null : Encoding.ASCII.GetString(buffer.ToArray()).TrimEnd('\r'); + } + + if (value == '\n') + { + return Encoding.ASCII.GetString(buffer.ToArray()).TrimEnd('\r'); + } + + buffer.WriteByte((byte)value); + } + } +} diff --git a/src/FScript.LanguageServer/LspHandlers.cs b/src/FScript.LanguageServer/LspHandlers.cs new file mode 100644 index 0000000..72ddd48 --- /dev/null +++ b/src/FScript.LanguageServer/LspHandlers.cs @@ -0,0 +1,266 @@ +using System.Text.Json.Nodes; +using FScript.CSharpInterop; +using FScript.Language; +using Microsoft.FSharp.Core; + +namespace FScript.LanguageServer.CSharp; + +internal static class LspHandlers +{ + private static readonly Position FallbackPosition = new(FSharpOption.None, 1, 1); + private static readonly Span FallbackSpan = new(FallbackPosition, FallbackPosition); + + internal static JsonObject CreateInitializeResult() + { + var sync = new JsonObject + { + ["openClose"] = true, + ["change"] = 1 + }; + + var capabilities = new JsonObject + { + ["textDocumentSync"] = sync, + ["hoverProvider"] = true + }; + + return new JsonObject + { + ["capabilities"] = capabilities, + ["serverInfo"] = new JsonObject + { + ["name"] = "FScript Language Server (C#)" + } + }; + } + + internal static JsonObject HandleStdlibSource(JsonObject? @params) + { + var uri = @params?["uri"]?.GetValue(); + if (string.IsNullOrWhiteSpace(uri)) + { + return Error("internal", "Missing stdlib URI."); + } + + var textOption = InteropServices.tryLoadStdlibSourceText(uri); + if (textOption is null) + { + return Error("internal", $"Unable to load stdlib source for '{uri}'."); + } + + var text = textOption.Value; + return new JsonObject + { + ["ok"] = true, + ["data"] = new JsonObject + { + ["uri"] = uri, + ["text"] = text, + ["languageId"] = "fscript" + } + }; + } + + internal static JsonObject HandleViewAst(JsonObject? @params, Func tryLoadSource) + { + var uri = TryGetCommandUri(@params); + if (string.IsNullOrWhiteSpace(uri)) + { + return Error("internal", "Missing document URI."); + } + + if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsed) || + !string.Equals(parsed.Scheme, "file", StringComparison.OrdinalIgnoreCase)) + { + return Error("internal", "AST commands support file-based scripts only."); + } + + var sourcePath = parsed.LocalPath; + var sourceText = tryLoadSource(uri); + if (sourceText is null) + { + return Error("internal", $"Unable to read source file '{sourcePath}'."); + } + + try + { + var program = InteropServices.parseProgramFromSourceWithIncludes(sourcePath, sourceText); + var data = global::FScript.LanguageServer.AstJson.programToJson(sourcePath, program); + return new JsonObject + { + ["ok"] = true, + ["data"] = data + }; + } + catch (ParseException ex) + { + return Error("parse", ex.Message); + } + catch (Exception ex) + { + return Error("internal", ex.Message); + } + } + + internal static JsonObject HandleViewInferredAst(JsonObject? @params, Func tryLoadSource) + { + var uri = TryGetCommandUri(@params); + if (string.IsNullOrWhiteSpace(uri)) + { + return Error("internal", "Missing document URI."); + } + + if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsed) || + !string.Equals(parsed.Scheme, "file", StringComparison.OrdinalIgnoreCase)) + { + return Error("internal", "AST commands support file-based scripts only."); + } + + var sourcePath = parsed.LocalPath; + var sourceText = tryLoadSource(uri); + if (sourceText is null) + { + return Error("internal", $"Unable to read source file '{sourcePath}'."); + } + + try + { + var program = InteropServices.parseProgramFromSourceWithIncludes(sourcePath, sourceText); + var externs = InteropServices.runtimeExternsForSourcePath(sourcePath); + var typed = InteropServices.inferProgramWithExterns(externs, program); + var data = global::FScript.LanguageServer.AstJson.typedProgramToJson(sourcePath, typed); + return new JsonObject + { + ["ok"] = true, + ["data"] = data + }; + } + catch (ParseException ex) + { + return Error("parse", ex.Message); + } + catch (TypeException ex) + { + return Error("type", ex.Message); + } + catch (Exception ex) + { + return Error("internal", ex.Message); + } + } + + internal static JsonObject CreateDiagnosticsParams(string uri, string text) + { + var diagnostics = new JsonArray(); + var sourcePath = ResolveSourcePath(uri); + + try + { + var program = InteropServices.parseProgramFromSourceWithIncludes(sourcePath, text); + var externs = InteropServices.runtimeExternsForSourcePath(sourcePath); + _ = InteropServices.inferProgramWithExterns(externs, program); + } + catch (ParseException ex) + { + diagnostics.Add(CreateDiagnostic("parse", ex.Message, ExtractSpan(ex))); + } + catch (TypeException ex) + { + diagnostics.Add(CreateDiagnostic("type", ex.Message, ExtractSpan(ex))); + } + + return new JsonObject + { + ["uri"] = uri, + ["diagnostics"] = diagnostics + }; + } + + private static string ResolveSourcePath(string uri) + { + if (Uri.TryCreate(uri, UriKind.Absolute, out var parsed) && + string.Equals(parsed.Scheme, "file", StringComparison.OrdinalIgnoreCase)) + { + return parsed.LocalPath; + } + + return uri; + } + + private static JsonObject CreateDiagnostic(string code, string message, Span span) + { + var startLine = Math.Max(0, span.Start.Line - 1); + var startCharacter = Math.Max(0, span.Start.Column - 1); + var endLine = Math.Max(0, span.End.Line - 1); + var endCharacter = Math.Max(0, span.End.Column - 1); + + return new JsonObject + { + ["range"] = new JsonObject + { + ["start"] = new JsonObject + { + ["line"] = startLine, + ["character"] = startCharacter + }, + ["end"] = new JsonObject + { + ["line"] = endLine, + ["character"] = endCharacter + } + }, + ["severity"] = 1, + ["code"] = code, + ["source"] = "fscript-lsp", + ["message"] = message + }; + } + + private static Span ExtractSpan(Exception ex) + { + try + { + var data0 = ex.GetType().GetProperty("Data0")?.GetValue(ex); + if (data0 is null) + { + return FallbackSpan; + } + + var spanObj = data0.GetType().GetProperty("Span")?.GetValue(data0); + if (spanObj is Span span) + { + return span; + } + } + catch + { + // Ignore and fallback to default span. + } + + return FallbackSpan; + } + + private static string? TryGetCommandUri(JsonObject? @params) + { + var fromTextDocument = (@params?["textDocument"] as JsonObject)?["uri"]?.GetValue(); + if (!string.IsNullOrWhiteSpace(fromTextDocument)) + { + return fromTextDocument; + } + + return @params?["uri"]?.GetValue(); + } + + private static JsonObject Error(string kind, string message) + { + return new JsonObject + { + ["ok"] = false, + ["error"] = new JsonObject + { + ["kind"] = kind, + ["message"] = message + } + }; + } +} diff --git a/src/FScript.LanguageServer/LspRuntimeExterns.fs b/src/FScript.LanguageServer/LspRuntimeExterns.fs deleted file mode 100644 index 951e58c..0000000 --- a/src/FScript.LanguageServer/LspRuntimeExterns.fs +++ /dev/null @@ -1,20 +0,0 @@ -namespace FScript.LanguageServer - -open System -open System.IO -open FScript.Language -open FScript.Runtime - -module LspRuntimeExterns = - let private resolveRootDirectory (sourcePath: string) = - try - match Path.GetDirectoryName(sourcePath) with - | null - | "" -> Directory.GetCurrentDirectory() - | dir -> dir - with _ -> - Directory.GetCurrentDirectory() - - let forSourcePath (sourcePath: string) : ExternalFunction list = - let ctx = { HostContext.RootDirectory = resolveRootDirectory sourcePath } - Registry.all ctx diff --git a/src/FScript.LanguageServer/LspServer.cs b/src/FScript.LanguageServer/LspServer.cs new file mode 100644 index 0000000..e37eb39 --- /dev/null +++ b/src/FScript.LanguageServer/LspServer.cs @@ -0,0 +1,176 @@ +using System.Text.Json.Nodes; +using Microsoft.FSharp.Core; +using FSLspHandlers = FScript.LanguageServer.LspHandlers; +using FSLspProtocol = FScript.LanguageServer.LspProtocol; + +namespace FScript.LanguageServer.CSharp; + +internal sealed class LspServer +{ + private bool _shutdownRequested; + + public void Run() + { + var input = Console.OpenStandardInput(); + + while (true) + { + var raw = JsonRpcWire.ReadMessage(input); + if (raw is null) + { + break; + } + + JsonObject? message; + try + { + message = JsonNode.Parse(raw) as JsonObject; + } + catch + { + continue; + } + + if (message is null) + { + continue; + } + + var method = message["method"]?.GetValue(); + var idNode = message["id"]; + var paramsObj = message["params"] as JsonObject; + + if (string.IsNullOrWhiteSpace(method)) + { + continue; + } + + try + { + if (idNode is null) + { + HandleNotification(method!, paramsObj); + if (_shutdownRequested && string.Equals(method, "exit", StringComparison.Ordinal)) + { + break; + } + } + else + { + HandleRequest(idNode, method!, paramsObj); + } + } + catch (Exception ex) + { + var payload = new JsonObject + { + ["type"] = 1, + ["message"] = $"FScript LSP internal error (C# host): {ex.Message}" + }; + FSLspProtocol.sendNotification("window/logMessage", FSharpOption.Some(payload)); + } + } + } + + private static void HandleNotification(string method, JsonObject? paramsObj) + { + switch (method) + { + case "initialized": + break; + case "textDocument/didOpen": + if (paramsObj is not null) + { + FSLspHandlers.handleDidOpen(paramsObj); + } + break; + case "textDocument/didChange": + if (paramsObj is not null) + { + FSLspHandlers.handleDidChange(paramsObj); + } + break; + case "textDocument/didClose": + if (paramsObj is not null) + { + FSLspHandlers.handleDidClose(paramsObj); + } + break; + case "exit": + break; + } + } + + private void HandleRequest(JsonNode idNode, string method, JsonObject? paramsObj) + { + switch (method) + { + case "initialize": + FSLspHandlers.handleInitialize(idNode, paramsObj is null ? FSharpOption.None : FSharpOption.Some(paramsObj)); + break; + case "shutdown": + _shutdownRequested = true; + FSLspProtocol.sendResponse(idNode, FSharpOption.None); + break; + case "textDocument/completion": + if (paramsObj is not null) FSLspHandlers.handleCompletion(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/semanticTokens/full": + if (paramsObj is not null) FSLspHandlers.handleSemanticTokens(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/hover": + if (paramsObj is not null) FSLspHandlers.handleHover(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/definition": + if (paramsObj is not null) FSLspHandlers.handleDefinition(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/typeDefinition": + if (paramsObj is not null) FSLspHandlers.handleTypeDefinition(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/documentSymbol": + if (paramsObj is not null) FSLspHandlers.handleDocumentSymbol(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/references": + if (paramsObj is not null) FSLspHandlers.handleReferences(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/documentHighlight": + if (paramsObj is not null) FSLspHandlers.handleDocumentHighlight(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/signatureHelp": + if (paramsObj is not null) FSLspHandlers.handleSignatureHelp(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/rename": + if (paramsObj is not null) FSLspHandlers.handleRename(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/prepareRename": + if (paramsObj is not null) FSLspHandlers.handlePrepareRename(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "workspace/symbol": + if (paramsObj is not null) FSLspHandlers.handleWorkspaceSymbol(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/codeAction": + if (paramsObj is not null) FSLspHandlers.handleCodeAction(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "textDocument/inlayHint": + if (paramsObj is not null) FSLspHandlers.handleInlayHints(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "fscript/viewAst": + if (paramsObj is not null) FSLspHandlers.handleViewAst(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "fscript/viewInferredAst": + if (paramsObj is not null) FSLspHandlers.handleViewInferredAst(idNode, paramsObj); else SendInvalidParams(idNode); + break; + case "fscript/stdlibSource": + if (paramsObj is not null) FSLspHandlers.handleStdlibSource(idNode, paramsObj); else SendInvalidParams(idNode); + break; + default: + FSLspProtocol.sendError(idNode, -32601, "Method not found"); + break; + } + } + + private static void SendInvalidParams(JsonNode idNode) + { + FSLspProtocol.sendError(idNode, -32602, "Invalid params"); + } +} diff --git a/src/FScript.LanguageServer/LspServer.fs b/src/FScript.LanguageServer/LspServer.fs deleted file mode 100644 index 7fb4220..0000000 --- a/src/FScript.LanguageServer/LspServer.fs +++ /dev/null @@ -1,90 +0,0 @@ -namespace FScript.LanguageServer - -open System -open System.Text.Json.Nodes - -module LspServer = - open LspModel - - let run () = - let mutable keepRunning = true - let mutable shutdownReceived = false - - while keepRunning do - match LspProtocol.tryReadMessage () with - | None -> keepRunning <- false - | Some payload -> - try - match JsonNode.Parse(payload) with - | null -> () - | rootNode -> - match asObject rootNode with - | None -> () - | Some root -> - let methodName = tryGetString root "method" - let idNode = tryGetNode root "id" - let paramsObj = tryGetObject root "params" - - match methodName, idNode, paramsObj with - | Some "initialize", Some idNode, paramsObj -> - LspHandlers.handleInitialize idNode paramsObj - | Some "initialized", _, _ -> - () - | Some "shutdown", Some idNode, _ -> - shutdownReceived <- true - LspProtocol.sendResponse idNode None - | Some "exit", _, _ -> - keepRunning <- false - if shutdownReceived then - Environment.ExitCode <- 0 - else - Environment.ExitCode <- 1 - | Some "textDocument/didOpen", _, Some paramsObj -> - LspHandlers.handleDidOpen paramsObj - | Some "textDocument/didChange", _, Some paramsObj -> - LspHandlers.handleDidChange paramsObj - | Some "textDocument/didClose", _, Some paramsObj -> - LspHandlers.handleDidClose paramsObj - | Some "textDocument/completion", Some idNode, Some paramsObj -> - LspHandlers.handleCompletion idNode paramsObj - | Some "textDocument/semanticTokens/full", Some idNode, Some paramsObj -> - LspHandlers.handleSemanticTokens idNode paramsObj - | Some "textDocument/hover", Some idNode, Some paramsObj -> - LspHandlers.handleHover idNode paramsObj - | Some "textDocument/definition", Some idNode, Some paramsObj -> - LspHandlers.handleDefinition idNode paramsObj - | Some "textDocument/typeDefinition", Some idNode, Some paramsObj -> - LspHandlers.handleTypeDefinition idNode paramsObj - | Some "textDocument/documentSymbol", Some idNode, Some paramsObj -> - LspHandlers.handleDocumentSymbol idNode paramsObj - | Some "textDocument/references", Some idNode, Some paramsObj -> - LspHandlers.handleReferences idNode paramsObj - | Some "textDocument/documentHighlight", Some idNode, Some paramsObj -> - LspHandlers.handleDocumentHighlight idNode paramsObj - | Some "textDocument/signatureHelp", Some idNode, Some paramsObj -> - LspHandlers.handleSignatureHelp idNode paramsObj - | Some "textDocument/rename", Some idNode, Some paramsObj -> - LspHandlers.handleRename idNode paramsObj - | Some "textDocument/prepareRename", Some idNode, Some paramsObj -> - LspHandlers.handlePrepareRename idNode paramsObj - | Some "workspace/symbol", Some idNode, Some paramsObj -> - LspHandlers.handleWorkspaceSymbol idNode paramsObj - | Some "textDocument/codeAction", Some idNode, Some paramsObj -> - LspHandlers.handleCodeAction idNode paramsObj - | Some "textDocument/inlayHint", Some idNode, Some paramsObj -> - LspHandlers.handleInlayHints idNode paramsObj - | Some "fscript/viewAst", Some idNode, Some paramsObj -> - LspHandlers.handleViewAst idNode paramsObj - | Some "fscript/viewInferredAst", Some idNode, Some paramsObj -> - LspHandlers.handleViewInferredAst idNode paramsObj - | Some "fscript/stdlibSource", Some idNode, Some paramsObj -> - LspHandlers.handleStdlibSource idNode paramsObj - | Some _, Some idNode, _ -> - LspProtocol.sendError idNode -32601 "Method not found" - | _ -> () - with ex -> - // Never crash the server loop on malformed input. - let p = JsonObject() - p["type"] <- JsonValue.Create(1) - p["message"] <- JsonValue.Create($"FScript LSP internal error: {ex.Message}") - LspProtocol.sendNotification "window/logMessage" (Some p) diff --git a/src/FScript.LanguageServer/Program.cs b/src/FScript.LanguageServer/Program.cs new file mode 100644 index 0000000..ddfee51 --- /dev/null +++ b/src/FScript.LanguageServer/Program.cs @@ -0,0 +1,4 @@ +using FScript.LanguageServer.CSharp; + +var server = new LspServer(); +server.Run(); diff --git a/src/FScript.LanguageServer/Program.fs b/src/FScript.LanguageServer/Program.fs deleted file mode 100644 index 5d24c3c..0000000 --- a/src/FScript.LanguageServer/Program.fs +++ /dev/null @@ -1,6 +0,0 @@ -module FScript.LanguageServer.Program - -[] -let main _ = - LspServer.run () - 0 diff --git a/tests/FScript.Language.Tests/IncludeResolverTests.fs b/tests/FScript.Language.Tests/IncludeResolverTests.fs index 01a2de2..79eb03f 100644 --- a/tests/FScript.Language.Tests/IncludeResolverTests.fs +++ b/tests/FScript.Language.Tests/IncludeResolverTests.fs @@ -186,3 +186,18 @@ type IncludeResolverTests () = let act () = IncludeResolver.parseProgramFromFile dir mainPath |> ignore act |> should throw typeof) + + [] + member _.``Import inference resolves qualified type annotation to imported type`` () = + withTempDir (fun dir -> + let commonPath = Path.Combine(dir, "common.fss") + let mainPath = Path.Combine(dir, "main.fss") + + File.WriteAllText(commonPath, "type ProjectInfo = { Name: string; Language: string }\nlet describe_project (project: ProjectInfo) = project.Name") + File.WriteAllText(mainPath, "import \"common.fss\"\nlet summary (project: common.ProjectInfo) = common.describe_project project\nsummary { Name = \"Terrabuild\"; Language = \"F#\" }") + + let program = IncludeResolver.parseProgramFromFile dir mainPath + let typed = TypeInfer.inferProgram program + match typed |> List.last with + | TypeInfer.TSExpr expr -> expr.Type |> should equal TString + | _ -> Assert.Fail("Expected expression")) diff --git a/tests/FScript.Language.Tests/ParserTests.fs b/tests/FScript.Language.Tests/ParserTests.fs index 3650b04..4cfefd2 100644 --- a/tests/FScript.Language.Tests/ParserTests.fs +++ b/tests/FScript.Language.Tests/ParserTests.fs @@ -324,6 +324,13 @@ type ParserTests () = | SLet ("format_address", [ { Name = "address"; Annotation = Some (TRStructuralRecord [ ("City", TRName "string"); ("Zip", TRName "int") ]) } ], _, _, _, _) -> () | _ -> Assert.Fail("Expected annotated let parameter with structural inline record type") + [] + member _.``Parses annotated parameter with qualified type name`` () = + let p = Helpers.parse "let summary (project: common.ProjectInfo) = project.Name" + match p.[0] with + | SLet ("summary", [ { Name = "project"; Annotation = Some (TRName "common.ProjectInfo") } ], _, _, _, _) -> () + | _ -> Assert.Fail("Expected annotated let parameter with qualified type name") + [] member _.``Parses structural record literal expression`` () = let p = Helpers.parse "let officeAddress = {| City = \"London\"; Zip = 12345 |}" diff --git a/tests/FScript.LanguageServer.Tests/CSharpServerCoreTests.cs b/tests/FScript.LanguageServer.Tests/CSharpServerCoreTests.cs new file mode 100644 index 0000000..9551b3e --- /dev/null +++ b/tests/FScript.LanguageServer.Tests/CSharpServerCoreTests.cs @@ -0,0 +1,209 @@ +using System.Text.Json.Nodes; +using NUnit.Framework; + +namespace FScript.LanguageServer.Tests; + +[TestFixture] +public sealed class CSharpServerCoreTests +{ + [Test] + public void CSharp_server_initialize_returns_capabilities() + { + var client = LspClient.StartCSharp(); + try + { + LspTestFixture.Initialize(client); + + var hoverReq = new JsonObject + { + ["textDocument"] = new JsonObject { ["uri"] = "file:///tmp/test.fss" }, + ["position"] = new JsonObject { ["line"] = 0, ["character"] = 0 } + }; + + LspClient.SendRequest(client, 42, "textDocument/hover", hoverReq); + var hoverResp = LspClient.ReadUntil(client, 10_000, msg => msg["id"] is JsonValue idv && idv.TryGetValue(out var id) && id == 42); + Assert.That(hoverResp["result"], Is.Null); + } + finally + { + try { LspTestFixture.Shutdown(client); } catch { } + LspClient.Stop(client); + } + } + + [Test] + public void CSharp_server_returns_stdlib_source() + { + var client = LspClient.StartCSharp(); + try + { + LspTestFixture.Initialize(client); + var requestParams = new JsonObject { ["uri"] = "fscript-stdlib:///Option.fss" }; + + LspClient.SendRequest(client, 43, "fscript/stdlibSource", requestParams); + var resp = LspClient.ReadUntil(client, 10_000, msg => msg["id"] is JsonValue idv && idv.TryGetValue(out var id) && id == 43); + + var result = resp["result"] as JsonObject ?? throw new Exception("Expected result object"); + Assert.That(result["ok"]?.GetValue(), Is.True); + var data = result["data"] as JsonObject ?? throw new Exception("Expected data object"); + var text = data["text"]?.GetValue() ?? string.Empty; + Assert.That(text.Contains("let", StringComparison.Ordinal), Is.True); + } + finally + { + try { LspTestFixture.Shutdown(client); } catch { } + LspClient.Stop(client); + } + } + + [Test] + public void CSharp_server_returns_method_not_found_for_unknown_request() + { + var client = LspClient.StartCSharp(); + try + { + LspTestFixture.Initialize(client); + LspClient.SendRequest(client, 44, "fscript/unknown", null); + var resp = LspClient.ReadUntil(client, 10_000, msg => msg["id"] is JsonValue idv && idv.TryGetValue(out var id) && id == 44); + var err = resp["error"] as JsonObject ?? throw new Exception("Expected error object"); + Assert.That(err["code"]?.GetValue(), Is.EqualTo(-32601)); + } + finally + { + try { LspTestFixture.Shutdown(client); } catch { } + LspClient.Stop(client); + } + } + + [Test] + public void CSharp_server_didOpen_publishes_parse_diagnostics() + { + var client = LspClient.StartCSharp(); + try + { + LspTestFixture.Initialize(client); + + var uri = "file:///tmp/csharp-diagnostics-test.fss"; + var didOpenParams = new JsonObject + { + ["textDocument"] = new JsonObject + { + ["uri"] = uri, + ["languageId"] = "fscript", + ["version"] = 1, + ["text"] = "let x =" + } + }; + LspClient.SendNotification(client, "textDocument/didOpen", didOpenParams); + + var diagMsg = LspClient.ReadUntil(client, 10_000, msg => + { + if (msg["method"]?.GetValue() != "textDocument/publishDiagnostics") + { + return false; + } + + var p = msg["params"] as JsonObject; + var u = p?["uri"]?.GetValue(); + var diagnostics = p?["diagnostics"] as JsonArray; + return u == uri && diagnostics is { Count: > 0 }; + }); + + var hasParseCode = false; + var paramsObj = diagMsg["params"] as JsonObject; + var diagnosticsArray = paramsObj?["diagnostics"] as JsonArray; + if (diagnosticsArray is not null) + { + foreach (var diag in diagnosticsArray) + { + if (diag is JsonObject d && d["code"]?.GetValue() == "parse") + { + hasParseCode = true; + break; + } + } + } + + Assert.That(hasParseCode, Is.True); + } + finally + { + try { LspTestFixture.Shutdown(client); } catch { } + LspClient.Stop(client); + } + } + + [Test] + public void CSharp_server_viewAst_returns_program_json() + { + var client = LspClient.StartCSharp(); + try + { + LspTestFixture.Initialize(client); + + var uri = "file:///tmp/csharp-view-ast-test.fss"; + var source = "let value = 42\nvalue\n"; + var didOpenParams = new JsonObject + { + ["textDocument"] = new JsonObject + { + ["uri"] = uri, + ["languageId"] = "fscript", + ["version"] = 1, + ["text"] = source + } + }; + LspClient.SendNotification(client, "textDocument/didOpen", didOpenParams); + _ = LspClient.ReadUntil(client, 10_000, msg => msg["method"]?.GetValue() == "textDocument/publishDiagnostics"); + + var requestParams = new JsonObject { ["textDocument"] = new JsonObject { ["uri"] = uri } }; + LspClient.SendRequest(client, 45, "fscript/viewAst", requestParams); + var response = LspClient.ReadUntil(client, 10_000, msg => msg["id"] is JsonValue idv && idv.TryGetValue(out var id) && id == 45); + + var kindValue = ((response["result"] as JsonObject)?["data"] as JsonObject)?["kind"]?.GetValue(); + Assert.That(kindValue, Is.EqualTo("program")); + } + finally + { + try { LspTestFixture.Shutdown(client); } catch { } + LspClient.Stop(client); + } + } + + [Test] + public void CSharp_server_viewInferredAst_returns_typed_program_json() + { + var client = LspClient.StartCSharp(); + try + { + LspTestFixture.Initialize(client); + + var uri = "file:///tmp/csharp-view-inferred-test.fss"; + var source = "let inc x = x + 1\ninc 1\n"; + var didOpenParams = new JsonObject + { + ["textDocument"] = new JsonObject + { + ["uri"] = uri, + ["languageId"] = "fscript", + ["version"] = 1, + ["text"] = source + } + }; + LspClient.SendNotification(client, "textDocument/didOpen", didOpenParams); + _ = LspClient.ReadUntil(client, 10_000, msg => msg["method"]?.GetValue() == "textDocument/publishDiagnostics"); + + var requestParams = new JsonObject { ["textDocument"] = new JsonObject { ["uri"] = uri } }; + LspClient.SendRequest(client, 46, "fscript/viewInferredAst", requestParams); + var response = LspClient.ReadUntil(client, 10_000, msg => msg["id"] is JsonValue idv && idv.TryGetValue(out var id) && id == 46); + + var kindValue = ((response["result"] as JsonObject)?["data"] as JsonObject)?["kind"]?.GetValue(); + Assert.That(kindValue, Is.EqualTo("typedProgram")); + } + finally + { + try { LspTestFixture.Shutdown(client); } catch { } + LspClient.Stop(client); + } + } +} diff --git a/tests/FScript.LanguageServer.Tests/FScript.LanguageServer.Tests.fsproj b/tests/FScript.LanguageServer.Tests/FScript.LanguageServer.Tests.csproj similarity index 50% rename from tests/FScript.LanguageServer.Tests/FScript.LanguageServer.Tests.fsproj rename to tests/FScript.LanguageServer.Tests/FScript.LanguageServer.Tests.csproj index 31dc4b9..7ae3b9b 100644 --- a/tests/FScript.LanguageServer.Tests/FScript.LanguageServer.Tests.fsproj +++ b/tests/FScript.LanguageServer.Tests/FScript.LanguageServer.Tests.csproj @@ -1,32 +1,21 @@ - net10.0 false - false enable + enable - - - - - - - - - - - - - - + + + + diff --git a/tests/FScript.LanguageServer.Tests/InteropServicesTests.cs b/tests/FScript.LanguageServer.Tests/InteropServicesTests.cs new file mode 100644 index 0000000..2da350a --- /dev/null +++ b/tests/FScript.LanguageServer.Tests/InteropServicesTests.cs @@ -0,0 +1,27 @@ +using FScript.CSharpInterop; +using NUnit.Framework; + +namespace FScript.LanguageServer.Tests; + +[TestFixture] +public sealed class InteropServicesTests +{ + [Test] + public void Interop_loads_stdlib_virtual_source() + { + var source = InteropServices.tryLoadStdlibSourceText("fscript-stdlib:///Option.fss"); + Assert.That(source is not null, Is.True); + } + + [Test] + public void Interop_parses_and_infers_a_simple_script() + { + const string script = "let add x y = x + y"; + const string sourcePath = "/tmp/interop-test.fss"; + var externs = InteropServices.runtimeExternsForSourcePath(sourcePath); + var program = InteropServices.parseProgramFromSourceWithIncludes(sourcePath, script); + var inferred = InteropServices.inferProgramWithExternsAndLocalVariableTypes(externs, program); + var typed = inferred.Item1; + Assert.That(typed, Is.Not.Null); + } +} diff --git a/tests/FScript.LanguageServer.Tests/LspClient.cs b/tests/FScript.LanguageServer.Tests/LspClient.cs new file mode 100644 index 0000000..501978a --- /dev/null +++ b/tests/FScript.LanguageServer.Tests/LspClient.cs @@ -0,0 +1,158 @@ +using System.Diagnostics; +using System.Text.Json.Nodes; + +namespace FScript.LanguageServer.Tests; + +internal static class LspClient +{ + internal sealed class Client + { + public required Process Process { get; init; } + public required Stream Input { get; init; } + public required Stream Output { get; init; } + } + + private static string FindRepoRoot() + { + DirectoryInfo? current = new(AppContext.BaseDirectory); + while (current is not null) + { + var candidate = Path.Combine(current.FullName, "FScript.sln"); + if (File.Exists(candidate)) + { + return current.FullName; + } + + current = current.Parent; + } + + throw new Exception("Unable to locate repository root from test base directory"); + } + + private static readonly Lazy EnsureServerDllBuilt = new(() => + { + var root = FindRepoRoot(); + var serverProject = Path.Combine(root, "src", "FScript.LanguageServer", "FScript.LanguageServer.csproj"); + var serverDll = Path.Combine(root, "src", "FScript.LanguageServer", "bin", "Release", "net10.0", "FScript.LanguageServer.dll"); + + var buildPsi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"build \"{serverProject}\" -c Release -nologo -v q", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var buildProc = new Process { StartInfo = buildPsi }; + if (!buildProc.Start()) + { + throw new Exception("Unable to start dotnet build for C# language server test setup."); + } + + buildProc.WaitForExit(); + if (buildProc.ExitCode != 0 || !File.Exists(serverDll)) + { + var output = buildProc.StandardOutput.ReadToEnd(); + var err = buildProc.StandardError.ReadToEnd(); + throw new Exception($"Failed to build C# language server test target. stdout: {output}\nstderr: {err}"); + } + + return serverDll; + }); + + public static Client Start() + { + var serverDll = EnsureServerDllBuilt.Value; + + var psi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"\"{serverDll}\"", + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var proc = new Process { StartInfo = psi }; + if (!proc.Start()) + { + throw new Exception("Unable to start FScript C# language server process"); + } + + return new Client + { + Process = proc, + Input = proc.StandardInput.BaseStream, + Output = proc.StandardOutput.BaseStream + }; + } + + public static Client StartCSharp() => Start(); + public static Client StartFSharp() => Start(); + + public static void Stop(Client client) + { + if (!client.Process.HasExited) + { + try + { + client.Process.Kill(true); + } + catch + { + // Ignore + } + } + + client.Process.Dispose(); + } + + public static void SendRequest(Client client, int id, string methodName, JsonNode? parameters) + { + var payload = new JsonObject + { + ["jsonrpc"] = "2.0", + ["id"] = id, + ["method"] = methodName, + ["params"] = parameters ?? new JsonObject() + }; + LspWire.WriteMessage(client.Input, payload.ToJsonString()); + } + + public static void SendNotification(Client client, string methodName, JsonNode? parameters) + { + var payload = new JsonObject + { + ["jsonrpc"] = "2.0", + ["method"] = methodName, + ["params"] = parameters ?? new JsonObject() + }; + LspWire.WriteMessage(client.Input, payload.ToJsonString()); + } + + public static JsonObject ReadUntil(Client client, int timeoutMs, Func predicate) + { + var deadline = DateTime.UtcNow.AddMilliseconds(timeoutMs); + while (DateTime.UtcNow < deadline) + { + var remaining = (int)(deadline - DateTime.UtcNow).TotalMilliseconds; + if (remaining <= 0) + { + break; + } + + var raw = LspWire.ReadMessageWithTimeout(client.Output, remaining); + var node = JsonNode.Parse(raw); + if (node is JsonObject obj && predicate(obj)) + { + return obj; + } + } + + throw new Exception("Timed out waiting for expected LSP message"); + } +} diff --git a/tests/FScript.LanguageServer.Tests/LspCompletionAndSignatureTests.fs b/tests/FScript.LanguageServer.Tests/LspCompletionAndSignatureTests.fs deleted file mode 100644 index 0e16036..0000000 --- a/tests/FScript.LanguageServer.Tests/LspCompletionAndSignatureTests.fs +++ /dev/null @@ -1,937 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -open LspTestFixture - -[] -type LspCompletionAndSignatureTests () = - [] - member _.``Inlay hints show map key union and unknown for unresolved map signature`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/inlay-hints-map-unknown-test.fss" - let source = - "let remove k m =\n" - + " match m with\n" - + " | { [key] = _; ..rest } when key = k -> rest\n" - + " | _ -> m\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(0) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(0) - endPos["character"] <- JsonValue.Create(30) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 43 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 43 with _ -> false - | _ -> false) - - let labels = - match resp["result"] with - | :? JsonArray as hints -> - hints - |> Seq.choose (fun hint -> - match hint with - | :? JsonObject as h -> - match h["label"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Seq.toList - | _ -> [] - - labels |> should contain ": int|string" - labels |> should contain ": unknown map" - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover shows signature for injected runtime extern function`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-injected-extern-test.fss" - let source = "let ok = Fs.exists \".\"\nok" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(13) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 70 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 70 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHover = - hoverValue.Contains("Fs.exists", StringComparison.Ordinal) - && hoverValue.Contains("string -> bool", StringComparison.Ordinal) - && hoverValue.Contains("injected-function", StringComparison.Ordinal) - - Assert.That(hasExpectedHover, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Completion includes local bindings and filters by prefix`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/completion-test.fss" - let source = "let alpha = 1\nlet beta = 2\nal" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - // Ignore diagnostics publish for this valid document. - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(2) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - - LspClient.sendRequest client 4 "textDocument/completion" (Some completionProbe) - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 4 with _ -> false - | _ -> false) - - let hasAlpha = - match completionResp["result"] with - | :? JsonObject as result -> - match result["items"] with - | :? JsonArray as items -> - items - |> Seq.exists (fun item -> - match item with - | :? JsonObject as o -> - match o["label"] with - | :? JsonValue as v -> - try v.GetValue() = "alpha" with _ -> false - | _ -> false - | _ -> false) - | _ -> false - | _ -> false - - hasAlpha |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Completion supports module-qualified stdlib symbols`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/module-completion-test.fss" - let source = "List.ma" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(7) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - - LspClient.sendRequest client 5 "textDocument/completion" (Some completionProbe) - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 5 with _ -> false - | _ -> false) - - let hasListMap = - match completionResp["result"] with - | :? JsonObject as result -> - match result["items"] with - | :? JsonArray as items -> - items - |> Seq.exists (fun item -> - match item with - | :? JsonObject as o -> - match o["label"] with - | :? JsonValue as v -> - try v.GetValue() = "List.map" with _ -> false - | _ -> false - | _ -> false) - | _ -> false - | _ -> false - - hasListMap |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Completion proposes record fields after dot`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/record-field-completion-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet home = { City = \"Paris\"; Zip = 75000 }\nhome.City" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(5) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - - LspClient.sendRequest client 19 "textDocument/completion" (Some completionProbe) - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 19 with _ -> false - | _ -> false) - - let labels = - match completionResp["result"] with - | :? JsonObject as result -> - match result["items"] with - | :? JsonArray as items -> - items - |> Seq.choose (fun item -> - match item with - | :? JsonObject as o -> - match o["label"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Seq.toList - | _ -> [] - | _ -> [] - - labels |> should contain "City" - labels |> should contain "Zip" - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Completion filters record fields by dotted member prefix`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/record-field-prefix-completion-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet home = { City = \"Paris\"; Zip = 75000 }\nhome.City" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(6) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - - LspClient.sendRequest client 20 "textDocument/completion" (Some completionProbe) - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 20 with _ -> false - | _ -> false) - - let labels = - match completionResp["result"] with - | :? JsonObject as result -> - match result["items"] with - | :? JsonArray as items -> - items - |> Seq.choose (fun item -> - match item with - | :? JsonObject as o -> - match o["label"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Seq.toList - | _ -> [] - | _ -> [] - - labels |> should equal [ "City" ] - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Completion proposes fields for annotated function parameters`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/record-param-completion-test.fss" - let source = - "type Address = { City: string; Zip: int }\nlet format (address: Address) =\n address.City" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(13) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - - LspClient.sendRequest client 22 "textDocument/completion" (Some completionProbe) - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 22 with _ -> false - | _ -> false) - - let labels = - match completionResp["result"] with - | :? JsonObject as result -> - match result["items"] with - | :? JsonArray as items -> - items - |> Seq.choose (fun item -> - match item with - | :? JsonObject as o -> - match o["label"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Seq.toList - | _ -> [] - | _ -> [] - - labels |> should equal [ "City" ] - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Completion ranks symbol matches before keywords for non-empty prefix`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/completion-ranking-test.fss" - let source = "let alpha = 1\nlet alphabet = 2\nal" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(2) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - - LspClient.sendRequest client 24 "textDocument/completion" (Some completionProbe) - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 24 with _ -> false - | _ -> false) - - let firstLabel = - match completionResp["result"] with - | :? JsonObject as result -> - match result["items"] with - | :? JsonArray as items when items.Count > 0 -> - match items[0] with - | :? JsonObject as o -> - match o["label"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None - | _ -> None - | _ -> None - - firstLabel |> should equal (Some "alpha") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Completion marks exact symbol match as preselected and provides sort metadata`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/completion-preselect-test.fss" - let source = "let alpha = 1\nalpha" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(5) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - - LspClient.sendRequest client 28 "textDocument/completion" (Some completionProbe) - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 28 with _ -> false - | _ -> false) - - let alphaMeta = - match completionResp["result"] with - | :? JsonObject as result -> - match result["items"] with - | :? JsonArray as items -> - items - |> Seq.tryPick (fun item -> - match item with - | :? JsonObject as o -> - match o["label"] with - | :? JsonValue as labelV when (try labelV.GetValue() = "alpha" with _ -> false) -> - let preselected = - match o["preselect"] with - | :? JsonValue as pv -> (try Some (pv.GetValue()) with _ -> None) - | _ -> None - let hasSortText = - match o["sortText"] with - | :? JsonValue as sv -> (try not (String.IsNullOrWhiteSpace(sv.GetValue())) with _ -> false) - | _ -> false - Some (preselected, hasSortText) - | _ -> None - | _ -> None) - | _ -> None - | _ -> None - - alphaMeta |> should equal (Some (Some true, true)) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Signature help returns function signature for call target`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/signature-help-test.fss" - let source = "let add x y = x + y\nadd(1, 2)" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let sigParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(4) - sigParams["textDocument"] <- textDocument - sigParams["position"] <- position - let ctx = JsonObject() - ctx["triggerCharacter"] <- JsonValue.Create("(") - sigParams["context"] <- ctx - - LspClient.sendRequest client 7 "textDocument/signatureHelp" (Some sigParams) - let sigResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 7 with _ -> false - | _ -> false) - - let hasAddSignature = - match sigResp["result"] with - | :? JsonObject as result -> - match result["signatures"] with - | :? JsonArray as signatures -> - signatures - |> Seq.exists (fun sigNode -> - match sigNode with - | :? JsonObject as sigObj -> - match sigObj["label"] with - | :? JsonValue as label -> - try label.GetValue().StartsWith("add", StringComparison.Ordinal) with _ -> false - | _ -> false - | _ -> false) - | _ -> false - | _ -> false - - hasAddSignature |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Signature help sets active parameter index from cursor position`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/signature-help-active-parameter-test.fss" - let source = "let add3 x y z = x + y + z\nadd3(1, 2, 3)" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let sigParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(11) - sigParams["textDocument"] <- textDocument - sigParams["position"] <- position - let ctx = JsonObject() - ctx["triggerCharacter"] <- JsonValue.Create(",") - sigParams["context"] <- ctx - - LspClient.sendRequest client 25 "textDocument/signatureHelp" (Some sigParams) - let sigResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 25 with _ -> false - | _ -> false) - - let activeParameter = - match sigResp["result"] with - | :? JsonObject as result -> - match result["activeParameter"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None - - activeParameter |> should equal (Some 2) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover returns markdown signature and kind`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-test.fss" - let source = "let double x = x * 2\nlet result = double 21" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(14) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 9 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 9 with _ -> false - | _ -> false) - - let hasExpectedHoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> - let text = value.GetValue() - text.Contains("double", StringComparison.Ordinal) - && text.Contains("function", StringComparison.Ordinal) - && text.Contains("defined at L", StringComparison.Ordinal) - | _ -> false - | _ -> false - | _ -> false - - hasExpectedHoverValue |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover shows typed signature for inferable function even when another binding has a type error`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-best-effort-type-signature-test.fss" - let source = - "type ActionContext = { Name: string }\n" - + "type ShellOperation = | Command of string\n" - + "let command_op command = Command command\n" - + "let bad = 1 + true\n" - + "let tool (context: ActionContext) (args: string option) =\n" - + " [command_op \"x\"]\n" - + "tool\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(6) - position["character"] <- JsonValue.Create(2) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 49 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 49 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - hoverValue.Contains("tool: (context: ActionContext) -> (args: string option) -> ShellOperation list", StringComparison.Ordinal) - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover shows typed signature for include-based script functions`` () = - let client = LspClient.start () - try - initialize client - - let tempRoot = Path.Combine(Path.GetTempPath(), $"fscript-lsp-hover-include-{Guid.NewGuid():N}") - Directory.CreateDirectory(tempRoot) |> ignore - let includeFile = Path.Combine(tempRoot, "_protocol.fss") - let mainFile = Path.Combine(tempRoot, "main.fss") - File.WriteAllText(includeFile, "type ActionContext = { Name: string }\ntype ProjectInfo = { Ok: bool }\n") - File.WriteAllText(mainFile, "import \"_protocol.fss\"\n[]\nlet defaults (context: ActionContext) =\n { Ok = true }\n") - - let uri = Uri(mainFile).AbsoluteUri - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(File.ReadAllText(mainFile)) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(8) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 46 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 46 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> - value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - let text = hoverValue - match hoverResp["result"] with - | :? JsonObject -> text.Contains("defaults: (context: ActionContext) -> ProjectInfo", StringComparison.Ordinal) - | _ -> false - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client diff --git a/tests/FScript.LanguageServer.Tests/LspCoreTests.fs b/tests/FScript.LanguageServer.Tests/LspCoreTests.fs deleted file mode 100644 index 8da2552..0000000 --- a/tests/FScript.LanguageServer.Tests/LspCoreTests.fs +++ /dev/null @@ -1,109 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -open LspTestFixture - -[] -type LspCoreTests () = - [] - member _.``Initialize returns capabilities`` () = - let client = LspClient.start () - try - initialize client - - let completionProbe = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create("file:///tmp/test.fss") - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(0) - completionProbe["textDocument"] <- textDocument - completionProbe["position"] <- position - LspClient.sendRequest client 3 "textDocument/completion" (Some completionProbe) - - let completionResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 3 with _ -> false - | _ -> false) - - completionResp["result"] |> should not' (equal null) - - let inlayReq = JsonObject() - let textDocument2 = JsonObject() - textDocument2["uri"] <- JsonValue.Create("file:///tmp/test.fss") - inlayReq["textDocument"] <- textDocument2 - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(0) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(0) - endPos["character"] <- JsonValue.Create(10) - range["start"] <- startPos - range["end"] <- endPos - inlayReq["range"] <- range - LspClient.sendRequest client 31 "textDocument/inlayHint" (Some inlayReq) - let inlayResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 31 with _ -> false - | _ -> false) - - inlayResp["result"] |> should not' (equal null) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Custom AST requests reject non-file URIs`` () = - let client = LspClient.start () - try - initialize client - - let requestParams = JsonObject() - requestParams["uri"] <- JsonValue.Create("untitled:ast-test") - - LspClient.sendRequest client 62 "fscript/viewAst" (Some requestParams) - let response = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 62 with _ -> false - | _ -> false) - - let okValue = - match response["result"] with - | :? JsonObject as result -> - match result["ok"] with - | :? JsonValue as okNode -> - try okNode.GetValue() with _ -> true - | _ -> true - | _ -> true - - let message = - match response["result"] with - | :? JsonObject as result -> - match result["error"] with - | :? JsonObject as error -> - match error["message"] with - | :? JsonValue as mv -> - try mv.GetValue() with _ -> "" - | _ -> "" - | _ -> "" - | _ -> "" - - Assert.That(okValue, Is.False) - Assert.That(message.Contains("file-based scripts only", StringComparison.Ordinal), Is.True) - finally - try shutdown client with _ -> () - LspClient.stop client diff --git a/tests/FScript.LanguageServer.Tests/LspCustomRequestsTests.fs b/tests/FScript.LanguageServer.Tests/LspCustomRequestsTests.fs deleted file mode 100644 index ff7ade2..0000000 --- a/tests/FScript.LanguageServer.Tests/LspCustomRequestsTests.fs +++ /dev/null @@ -1,186 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -open LspTestFixture - -[] -type LspCustomRequestsTests () = - [] - member _.``Custom request viewAst returns parsed program as JSON`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/view-ast-test.fss" - let source = "let value = 42\nvalue\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let requestParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - requestParams["textDocument"] <- textDocument - - LspClient.sendRequest client 60 "fscript/viewAst" (Some requestParams) - let response = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 60 with _ -> false - | _ -> false) - - let okValue = - match response["result"] with - | :? JsonObject as result -> - match result["ok"] with - | :? JsonValue as okNode -> - try okNode.GetValue() with _ -> false - | _ -> false - | _ -> false - - let kindValue = - match response["result"] with - | :? JsonObject as result -> - match result["data"] with - | :? JsonObject as data -> - match data["kind"] with - | :? JsonValue as k -> - try k.GetValue() with _ -> "" - | _ -> "" - | _ -> "" - | _ -> "" - - Assert.That(okValue, Is.True) - Assert.That(kindValue, Is.EqualTo("program")) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Custom request viewInferredAst returns typed program as JSON`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/view-inferred-ast-test.fss" - let source = "let inc x = x + 1\ninc 1\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let requestParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - requestParams["textDocument"] <- textDocument - - LspClient.sendRequest client 61 "fscript/viewInferredAst" (Some requestParams) - let response = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 61 with _ -> false - | _ -> false) - - let okValue = - match response["result"] with - | :? JsonObject as result -> - match result["ok"] with - | :? JsonValue as okNode -> - try okNode.GetValue() with _ -> false - | _ -> false - | _ -> false - - let kindValue = - match response["result"] with - | :? JsonObject as result -> - match result["data"] with - | :? JsonObject as data -> - match data["kind"] with - | :? JsonValue as k -> - try k.GetValue() with _ -> "" - | _ -> "" - | _ -> "" - | _ -> "" - - Assert.That(okValue, Is.True) - Assert.That(kindValue, Is.EqualTo("typedProgram")) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Custom request stdlibSource returns embedded source text`` () = - let client = LspClient.start () - try - initialize client - - let requestParams = JsonObject() - requestParams["uri"] <- JsonValue.Create("fscript-stdlib:///Option.fss") - - LspClient.sendRequest client 611 "fscript/stdlibSource" (Some requestParams) - let response = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 611 with _ -> false - | _ -> false) - - let okValue = - match response["result"] with - | :? JsonObject as result -> - match result["ok"] with - | :? JsonValue as okNode -> - try okNode.GetValue() with _ -> false - | _ -> false - | _ -> false - - let sourceText = - match response["result"] with - | :? JsonObject as result -> - match result["data"] with - | :? JsonObject as data -> - match data["text"] with - | :? JsonValue as textNode -> - try textNode.GetValue() with _ -> "" - | _ -> "" - | _ -> "" - | _ -> "" - - Assert.That(okValue, Is.True) - Assert.That(sourceText.Contains("let map mapper value", StringComparison.Ordinal), Is.True) - finally - try shutdown client with _ -> () - LspClient.stop client diff --git a/tests/FScript.LanguageServer.Tests/LspHoverAndInlayTests.fs b/tests/FScript.LanguageServer.Tests/LspHoverAndInlayTests.fs deleted file mode 100644 index eb973d9..0000000 --- a/tests/FScript.LanguageServer.Tests/LspHoverAndInlayTests.fs +++ /dev/null @@ -1,1090 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -open LspTestFixture - -[] -type LspHoverAndInlayTests () = - [] - member _.``Inlay hints return parameter labels for function calls`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/inlay-hints-params-test.fss" - let source = "let add x y = x + y\nlet z = add(1, 2)" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(1) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(1) - endPos["character"] <- JsonValue.Create(20) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 32 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 32 with _ -> false - | _ -> false) - - let labels = - match resp["result"] with - | :? JsonArray as hints -> - hints - |> Seq.choose (fun hint -> - match hint with - | :? JsonObject as h -> - match h["label"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Seq.toList - | _ -> [] - - labels |> should contain "x:" - labels |> should contain "y:" - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Inlay hints do not show parameter labels on typed function declarations`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/inlay-hints-typed-declaration-test.fss" - let source = - "type ActionContext = { Name: string }\n" - + "let defaults (context: ActionContext) = context.Name\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(1) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(1) - endPos["character"] <- JsonValue.Create(50) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 45 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 45 with _ -> false - | _ -> false) - - let labels = - match resp["result"] with - | :? JsonArray as hints -> - hints - |> Seq.choose (fun hint -> - match hint with - | :? JsonObject as h -> - match h["label"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Seq.toList - | _ -> [] - - labels |> should not' (contain "context:") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Inlay hints include inferred type for value bindings`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/inlay-hints-types-test.fss" - let source = "let answer = 42\nanswer" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(0) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(0) - endPos["character"] <- JsonValue.Create(20) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 33 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 33 with _ -> false - | _ -> false) - - let hasTypeHint = - match resp["result"] with - | :? JsonArray as hints -> - hints - |> Seq.exists (fun hint -> - match hint with - | :? JsonObject as h -> - match h["label"] with - | :? JsonValue as v -> - try v.GetValue() = ": int" with _ -> false - | _ -> false - | _ -> false) - | _ -> false - - hasTypeHint |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Inlay hints include inferred type for lambda parameter`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/inlay-hints-lambda-param-test.fss" - let source = "let inc = fun x -> x + 1\ninc 2" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(0) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(0) - endPos["character"] <- JsonValue.Create(30) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 34 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 34 with _ -> false - | _ -> false) - - let hasLambdaParamTypeHint = - match resp["result"] with - | :? JsonArray as hints -> - hints - |> Seq.exists (fun hint -> - match hint with - | :? JsonObject as h -> - match h["label"] with - | :? JsonValue as v -> - try v.GetValue() = ": int" with _ -> false - | _ -> false - | _ -> false) - | _ -> false - - hasLambdaParamTypeHint |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Inlay hints include inferred return type for function declarations`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/inlay-hints-function-return-test.fss" - let source = "let is_empty values = values = []\nis_empty []" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(0) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(0) - endPos["character"] <- JsonValue.Create(40) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 48 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 48 with _ -> false - | _ -> false) - - let hasBoolReturnHint = - match resp["result"] with - | :? JsonArray as hints -> - hints - |> Seq.exists (fun hint -> - match hint with - | :? JsonObject as h -> - match h["label"] with - | :? JsonValue as v -> - try v.GetValue() = ": bool" with _ -> false - | _ -> false - | _ -> false) - | _ -> false - - hasBoolReturnHint |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Inlay hints include inferred type for option pattern variable`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/inlay-hints-option-pattern-var-test.fss" - let source = - "let firstEven = Some 2\n" - + "match firstEven with\n" - + "| Some x -> x\n" - + "| None -> 0\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(2) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(2) - endPos["character"] <- JsonValue.Create(20) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 44 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 44 with _ -> false - | _ -> false) - - let hasPatternVarTypeHint = - match resp["result"] with - | :? JsonArray as hints -> - hints - |> Seq.exists (fun hint -> - match hint with - | :? JsonObject as h -> - match h["label"] with - | :? JsonValue as v -> - try v.GetValue() = ": int" with _ -> false - | _ -> false - | _ -> false) - | _ -> false - - hasPatternVarTypeHint |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Inlay hints can be disabled through initialization options`` () = - let client = LspClient.start () - try - let options = JsonObject() - options["inlayHintsEnabled"] <- JsonValue.Create(false) - initializeWith client (Some options) - - let uri = "file:///tmp/inlay-hints-disabled-test.fss" - let source = "let add x y = x + y\nlet z = add(1, 2)" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(1) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(1) - endPos["character"] <- JsonValue.Create(20) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - - LspClient.sendRequest client 35 "textDocument/inlayHint" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 35 with _ -> false - | _ -> false) - - let isEmpty = - match resp["result"] with - | :? JsonArray as hints -> (hints |> Seq.length) = 0 - | _ -> false - - isEmpty |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover shows named arguments for injected stdlib function`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-injected-stdlib-test.fss" - let source = "let value = Option.map (fun x -> x + 1) (Some 1)\nvalue" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(21) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 71 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 71 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHover = - hoverValue.Contains("Option.map:", StringComparison.Ordinal) - && hoverValue.Contains("(mapper:", StringComparison.Ordinal) - && hoverValue.Contains("(value:", StringComparison.Ordinal) - && hoverValue.Contains("injected-function", StringComparison.Ordinal) - - Assert.That(hasExpectedHover, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover shows function parameters when type inference is unavailable`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-function-params-fallback-test.fss" - let source = - "let bad = 1 + true\n" - + "let with_batch_projects context create_command =\n" - + " create_command context\n" - + "with_batch_projects\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(3) - position["character"] <- JsonValue.Create(5) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 47 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 47 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - hoverValue.Contains("with_batch_projects:", StringComparison.Ordinal) - && hoverValue.Contains("->", StringComparison.Ordinal) - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover returns record field information for dotted access`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-record-field-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet home = { City = \"Paris\"; Zip = 75000 }\nhome.City" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(7) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 30 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 30 with _ -> false - | _ -> false) - - let hasFieldHover = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> - let text = value.GetValue() - text.Contains("City : string", StringComparison.Ordinal) - && text.Contains("record-field", StringComparison.Ordinal) - | _ -> false - | _ -> false - | _ -> false - - hasFieldHover |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover returns inferred type for local lambda variables`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-local-variables-test.fss" - let source = - "let rec fib n = if n < 2 then n else fib (n - 1) + fib (n - 2)\n" - + "let values =\n" - + " [0..9]\n" - + " |> List.map (fun i ->\n" - + " i |> fib |> fun x ->\n" - + " $\"{x}\")\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let requestHover (requestId: int) (line: int) (character: int) = - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(line) - position["character"] <- JsonValue.Create(character) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - LspClient.sendRequest client requestId "textDocument/hover" (Some hoverParams) - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = requestId with _ -> false - | _ -> false) - - let hoverText (resp: JsonNode) = - match resp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasLocalHoverType (text: string) (name: string) (typeText: string) = - text.Contains($"{name} : {typeText}", StringComparison.Ordinal) - && text.Contains("local-variable", StringComparison.Ordinal) - - let hoverI = requestHover 41 3 21 - let hoverX = requestHover 42 4 24 - let hoverIText = hoverText hoverI - let hoverXText = hoverText hoverX - - Assert.That(hasLocalHoverType hoverIText "i" "int", Is.True, $"Unexpected hover for i: {hoverIText}") - Assert.That(hasLocalHoverType hoverXText "x" "int", Is.True, $"Unexpected hover for x: {hoverXText}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover returns local binding type even when another top-level binding has a type error`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-local-binding-best-effort-test.fss" - let source = - "let bad = 1 + true\n" - + "let restore context =\n" - + " let locked = context = \"locked\"\n" - + " if locked then \"x\" else \"y\"\n" - + "restore \"u\"\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(3) - position["character"] <- JsonValue.Create(8) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 50 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 50 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - hoverValue.Contains("locked : bool", StringComparison.Ordinal) - && hoverValue.Contains("local-variable", StringComparison.Ordinal) - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover on top-level function is not shadowed by same-name local binding`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-top-level-not-shadowed-by-local-test.fss" - let source = - "let restore (context: string option) =\n" - + " let restore = context\n" - + " restore\n" - + "restore None\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(5) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 51 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 51 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - hoverValue.Contains("restore:", StringComparison.Ordinal) - && hoverValue.Contains("local-variable", StringComparison.Ordinal) |> not - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover resolves nearest local binding when name is reused across functions`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-nearest-local-reused-name-test.fss" - let source = - "let first args =\n" - + " let flag = args = \"x\"\n" - + " flag\n" - + "let second args =\n" - + " let flag = args = 42\n" - + " flag\n" - + "second 42\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(5) - position["character"] <- JsonValue.Create(6) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 52 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 52 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - hoverValue.Contains("flag : bool", StringComparison.Ordinal) - && hoverValue.Contains("local-variable", StringComparison.Ordinal) - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover resolves local let declaration identifier`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-local-let-declaration-test.fss" - let source = - "let defaults context =\n" - + " let dependencies =\n" - + " if context then [] else []\n" - + " dependencies\n" - + "defaults true\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(9) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 53 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 53 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - hoverValue.Contains("dependencies : unknown list", StringComparison.Ordinal) - && hoverValue.Contains("local-variable", StringComparison.Ordinal) - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Hover infers local let type from returned record field when best-effort falls back`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/hover-local-let-return-record-field-test.fss" - let source = - "type ProjectInfo = { Id: string option; Outputs: string list; Dependencies: string list }\n" - + "let external_call x = missing_function x\n" - + "let defaults context =\n" - + " let dependencies =\n" - + " context\n" - + " |> external_call\n" - + " |> Option.defaultValue []\n" - + " { Id = None; Outputs = []; Dependencies = dependencies }\n" - + "defaults \"ok\"\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hoverParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(3) - position["character"] <- JsonValue.Create(9) - hoverParams["textDocument"] <- textDocument - hoverParams["position"] <- position - - LspClient.sendRequest client 54 "textDocument/hover" (Some hoverParams) - let hoverResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 54 with _ -> false - | _ -> false) - - let hoverValue = - match hoverResp["result"] with - | :? JsonObject as result -> - match result["contents"] with - | :? JsonObject as contents -> - match contents["value"] with - | :? JsonValue as value -> value.GetValue() - | _ -> "" - | _ -> "" - | _ -> "" - - let hasExpectedHoverValue = - hoverValue.Contains("dependencies : string list", StringComparison.Ordinal) - && hoverValue.Contains("local-variable", StringComparison.Ordinal) - - Assert.That(hasExpectedHoverValue, Is.True, $"Unexpected hover text: {hoverValue}") - finally - try shutdown client with _ -> () - LspClient.stop client diff --git a/tests/FScript.LanguageServer.Tests/LspNavigationTests.fs b/tests/FScript.LanguageServer.Tests/LspNavigationTests.fs deleted file mode 100644 index 1252e16..0000000 --- a/tests/FScript.LanguageServer.Tests/LspNavigationTests.fs +++ /dev/null @@ -1,981 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -open LspTestFixture - -[] -type LspNavigationTests () = - [] - member _.``References returns all occurrences for a top-level binding`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/references-test.fss" - let source = "let alpha x = x + 1\nlet v = alpha 41\nalpha v" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let refsParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(9) - refsParams["textDocument"] <- textDocument - refsParams["position"] <- position - let ctx = JsonObject() - ctx["includeDeclaration"] <- JsonValue.Create(true) - refsParams["context"] <- ctx - - LspClient.sendRequest client 6 "textDocument/references" (Some refsParams) - let refsResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 6 with _ -> false - | _ -> false) - - let count = - match refsResp["result"] with - | :? JsonArray as items -> items |> Seq.length - | _ -> 0 - - count |> should be (greaterThanOrEqualTo 3) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``References returns occurrences across opened documents`` () = - let client = LspClient.start () - try - initialize client - - let openDoc (uri: string) (source: string) = - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let sourceUri = "file:///tmp/references-source.fss" - let usageUri = "file:///tmp/references-usage.fss" - openDoc sourceUri "let alpha x = x + 1" - openDoc usageUri "let one = alpha 1\nlet two = alpha 2" - - let refsParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(usageUri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(11) - refsParams["textDocument"] <- textDocument - refsParams["position"] <- position - let ctx = JsonObject() - ctx["includeDeclaration"] <- JsonValue.Create(true) - refsParams["context"] <- ctx - - LspClient.sendRequest client 23 "textDocument/references" (Some refsParams) - let refsResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 23 with _ -> false - | _ -> false) - - let uris = - match refsResp["result"] with - | :? JsonArray as items -> - items - |> Seq.choose (fun item -> - match item with - | :? JsonObject as o -> - match o["uri"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Set.ofSeq - | _ -> Set.empty - - uris.Contains(sourceUri) |> should equal true - uris.Contains(usageUri) |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``References honors includeDeclaration false across opened documents`` () = - let client = LspClient.start () - try - initialize client - - let openDoc (uri: string) (source: string) = - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let sourceUri = "file:///tmp/references-nodecl-source.fss" - let usageUri = "file:///tmp/references-nodecl-usage.fss" - openDoc sourceUri "let alpha x = x + 1" - openDoc usageUri "let value = alpha 41" - - let refsParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(usageUri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(12) - refsParams["textDocument"] <- textDocument - refsParams["position"] <- position - let ctx = JsonObject() - ctx["includeDeclaration"] <- JsonValue.Create(false) - refsParams["context"] <- ctx - - LspClient.sendRequest client 27 "textDocument/references" (Some refsParams) - let refsResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 27 with _ -> false - | _ -> false) - - let uris = - match refsResp["result"] with - | :? JsonArray as items -> - items - |> Seq.choose (fun item -> - match item with - | :? JsonObject as o -> - match o["uri"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None) - |> Set.ofSeq - | _ -> Set.empty - - uris.Contains(sourceUri) |> should equal false - uris.Contains(usageUri) |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Definition resolves top-level binding usage`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/definition-test.fss" - let source = "let inc x = x + 1\nlet y = inc 41" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(11) - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 8 "textDocument/definition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 8 with _ -> false - | _ -> false) - - let isDefinitionOnFirstLine = - match defResp["result"] with - | :? JsonObject as result -> - let uriOk = - match result["uri"] with - | :? JsonValue as v -> (try v.GetValue() = uri with _ -> false) - | _ -> false - - let startLineOk = - match result["range"] with - | :? JsonObject as rangeObj -> - match rangeObj["start"] with - | :? JsonObject as startObj -> - match startObj["line"] with - | :? JsonValue as v -> (try v.GetValue() = 0 with _ -> false) - | _ -> false - | _ -> false - | _ -> false - uriOk && startLineOk - | _ -> false - - isDefinitionOnFirstLine |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Definition resolves injected stdlib function to virtual stdlib source`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/definition-injected-stdlib-test.fss" - let source = "let value = Option.map (fun x -> x + 1) (Some 1)\nvalue" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(21) - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 81 "textDocument/definition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 81 with _ -> false - | _ -> false) - - let resolvedUri = - match defResp["result"] with - | :? JsonObject as loc -> - match loc["uri"] with - | :? JsonValue as u -> - try Some (u.GetValue()) with _ -> None - | _ -> None - | _ -> None - - resolvedUri |> should equal (Some "fscript-stdlib:///Option.fss") - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Definition resolves symbol across opened documents`` () = - let client = LspClient.start () - try - initialize client - - let openDoc (uri: string) (source: string) = - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defUri = "file:///tmp/definition-source.fss" - let useUri = "file:///tmp/definition-usage.fss" - openDoc defUri "let alpha x = x + 1" - openDoc useUri "let result = alpha 41" - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(useUri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(14) - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 21 "textDocument/definition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 21 with _ -> false - | _ -> false) - - let resolvedUri = - match defResp["result"] with - | :? JsonObject as loc -> - match loc["uri"] with - | :? JsonValue as u -> - try Some (u.GetValue()) with _ -> None - | _ -> None - | _ -> None - - resolvedUri |> should equal (Some defUri) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Definition resolves include path to target file`` () = - let client = LspClient.start () - try - initialize client - - let tempRoot = Path.Combine(Path.GetTempPath(), $"fscript-lsp-include-{Guid.NewGuid():N}") - Directory.CreateDirectory(tempRoot) |> ignore - let includeFile = Path.Combine(tempRoot, "_helpers.fss") - let mainFile = Path.Combine(tempRoot, "main.fss") - File.WriteAllText(includeFile, "let helper = 42\n") - File.WriteAllText(mainFile, "import \"_helpers.fss\"\nlet x = helper\n") - - let uri = Uri(mainFile).AbsoluteUri - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(File.ReadAllText(mainFile)) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(13) - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 36 "textDocument/definition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 36 with _ -> false - | _ -> false) - - let expectedUri = Uri(includeFile).AbsoluteUri - let resolvedUri = - match defResp["result"] with - | :? JsonObject as result -> - match result["uri"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None - - resolvedUri |> should equal (Some expectedUri) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Definition on included record field usage opens included file`` () = - let client = LspClient.start () - try - initialize client - - let tempRoot = Path.Combine(Path.GetTempPath(), $"fscript-lsp-include-field-{Guid.NewGuid():N}") - Directory.CreateDirectory(tempRoot) |> ignore - let includeFile = Path.Combine(tempRoot, "_protocol.fss") - let mainFile = Path.Combine(tempRoot, "main.fss") - - File.WriteAllText(includeFile, "type ActionContext = { Command: string }\n") - File.WriteAllText(mainFile, "import \"_protocol.fss\"\nlet dispatch (context: ActionContext) = context.Command\n") - - let uri = Uri(mainFile).AbsoluteUri - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(File.ReadAllText(mainFile)) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(52) - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 360 "textDocument/definition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 360 with _ -> false - | _ -> false) - - let expectedUri = Uri(includeFile).AbsoluteUri - let resolvedUri = - match defResp["result"] with - | :? JsonObject as result -> - match result["uri"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None - - resolvedUri |> should equal (Some expectedUri) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Type definition resolves record value to declared record type`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/type-definition-record-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet home = { City = \"Paris\"; Zip = 75000 }\nlet zip = home.Zip" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(10) - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 16 "textDocument/typeDefinition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 16 with _ -> false - | _ -> false) - - let pointsToTypeDecl = - match defResp["result"] with - | :? JsonObject as result -> - let uriOk = - match result["uri"] with - | :? JsonValue as v -> (try v.GetValue() = uri with _ -> false) - | _ -> false - - let startLineOk = - match result["range"] with - | :? JsonObject as rangeObj -> - match rangeObj["start"] with - | :? JsonObject as startObj -> - match startObj["line"] with - | :? JsonValue as v -> (try v.GetValue() = 0 with _ -> false) - | _ -> false - | _ -> false - | _ -> false - uriOk && startLineOk - | _ -> false - pointsToTypeDecl |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Type definition resolves inline nominal record annotation to declared type`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/type-definition-inline-annotation-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet format_address (address: { City: string; Zip: int }) = $\"{address.City} ({address.Zip})\"" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(30) // City in annotation - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 160 "textDocument/typeDefinition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 160 with _ -> false - | _ -> false) - - let pointsToTypeDecl = - match defResp["result"] with - | :? JsonObject as result -> - let uriOk = - match result["uri"] with - | :? JsonValue as v -> (try v.GetValue() = uri with _ -> false) - | _ -> false - let startLineOk = - match result["range"] with - | :? JsonObject as rangeObj -> - match rangeObj["start"] with - | :? JsonObject as startObj -> - match startObj["line"] with - | :? JsonValue as v -> (try v.GetValue() = 0 with _ -> false) - | _ -> false - | _ -> false - | _ -> false - uriOk && startLineOk - | _ -> false - - pointsToTypeDecl |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Type definition resolves annotated parameter usage to declared type`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/type-definition-parameter-usage-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet format_address (address: { City: string; Zip: int }) = address.City" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(62) // address in address.City - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 161 "textDocument/typeDefinition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 161 with _ -> false - | _ -> false) - - let pointsToTypeDecl = - match defResp["result"] with - | :? JsonObject as result -> - let uriOk = - match result["uri"] with - | :? JsonValue as v -> (try v.GetValue() = uri with _ -> false) - | _ -> false - let startLineOk = - match result["range"] with - | :? JsonObject as rangeObj -> - match rangeObj["start"] with - | :? JsonObject as startObj -> - match startObj["line"] with - | :? JsonValue as v -> (try v.GetValue() = 0 with _ -> false) - | _ -> false - | _ -> false - | _ -> false - uriOk && startLineOk - | _ -> false - - pointsToTypeDecl |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Type definition resolves record literal call-argument field label to declared type`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/type-definition-record-call-arg-field-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet make_office_address (address: Address) = address\nlet officeAddress = make_office_address { City = \"London\"; Zip = 12345 }" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(43) // City in literal argument - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 162 "textDocument/typeDefinition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 162 with _ -> false - | _ -> false) - - let pointsToTypeDecl = - match defResp["result"] with - | :? JsonObject as result -> - let uriOk = - match result["uri"] with - | :? JsonValue as v -> (try v.GetValue() = uri with _ -> false) - | _ -> false - let startLineOk = - match result["range"] with - | :? JsonObject as rangeObj -> - match rangeObj["start"] with - | :? JsonObject as startObj -> - match startObj["line"] with - | :? JsonValue as v -> (try v.GetValue() = 0 with _ -> false) - | _ -> false - | _ -> false - | _ -> false - uriOk && startLineOk - | _ -> false - - pointsToTypeDecl |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Definition resolves record literal call-argument field label to declared type`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/definition-record-call-arg-field-test.fss" - let source = "type Address = { City: string; Zip: int }\nlet make_address (address: Address) = address\nlet office = make_address { City = \"London\"; Zip = 12345 }" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(33) // City in literal argument - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 163 "textDocument/definition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 163 with _ -> false - | _ -> false) - - let pointsToTypeDecl = - match defResp["result"] with - | :? JsonObject as result -> - let uriOk = - match result["uri"] with - | :? JsonValue as v -> (try v.GetValue() = uri with _ -> false) - | _ -> false - let startLineOk = - match result["range"] with - | :? JsonObject as rangeObj -> - match rangeObj["start"] with - | :? JsonObject as startObj -> - match startObj["line"] with - | :? JsonValue as v -> (try v.GetValue() = 0 with _ -> false) - | _ -> false - | _ -> false - | _ -> false - uriOk && startLineOk - | _ -> false - - pointsToTypeDecl |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Type definition resolves record literal binding field label to declared type`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/type-definition-record-binding-field-test.fss" - let source = "type Contact = { Name: string; City: string; Zip: int; Country: string }\nlet contact = { Name = \"Ada\"; City = \"Paris\"; Zip = 75000; Country = \"FR\" }" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(30) // City in literal binding - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 164 "textDocument/typeDefinition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 164 with _ -> false - | _ -> false) - - let pointsToTypeDecl = - match defResp["result"] with - | :? JsonObject as result -> - let uriOk = - match result["uri"] with - | :? JsonValue as v -> (try v.GetValue() = uri with _ -> false) - | _ -> false - let startLineOk = - match result["range"] with - | :? JsonObject as rangeObj -> - match rangeObj["start"] with - | :? JsonObject as startObj -> - match startObj["line"] with - | :? JsonValue as v -> (try v.GetValue() = 0 with _ -> false) - | _ -> false - | _ -> false - | _ -> false - uriOk && startLineOk - | _ -> false - - pointsToTypeDecl |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Definition resolves function return record field label to declared type in include file`` () = - let client = LspClient.start () - try - initialize client - - let tempRoot = Path.Combine(Path.GetTempPath(), $"fscript-lsp-type-definition-return-field-{Guid.NewGuid():N}") - Directory.CreateDirectory(tempRoot) |> ignore - let includeFile = Path.Combine(tempRoot, "_protocol.fss") - let mainFile = Path.Combine(tempRoot, "main.fss") - - File.WriteAllText(includeFile, "type ActionContext = { Directory: string }\ntype ProjectInfo = { Id: string option; Outputs: string list; Dependencies: string list }\n") - File.WriteAllText(mainFile, "import \"_protocol.fss\"\n[] let defaults (context: ActionContext) =\n let id = None\n { Id = id; Outputs = [\"dist/**\"]; Dependencies = [] }\n") - - let uri = Uri(mainFile).AbsoluteUri - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(File.ReadAllText(mainFile)) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let defParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(3) - position["character"] <- JsonValue.Create(4) - defParams["textDocument"] <- textDocument - defParams["position"] <- position - - LspClient.sendRequest client 364 "textDocument/definition" (Some defParams) - let defResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 364 with _ -> false - | _ -> false) - - let expectedUri = Uri(includeFile).AbsoluteUri - let resolvedUri = - match defResp["result"] with - | :? JsonObject as result -> - match result["uri"] with - | :? JsonValue as v -> - try Some (v.GetValue()) with _ -> None - | _ -> None - | _ -> None - - resolvedUri |> should equal (Some expectedUri) - finally - try shutdown client with _ -> () - LspClient.stop client diff --git a/tests/FScript.LanguageServer.Tests/LspSymbolsAndActionsTests.fs b/tests/FScript.LanguageServer.Tests/LspSymbolsAndActionsTests.fs deleted file mode 100644 index b17b466..0000000 --- a/tests/FScript.LanguageServer.Tests/LspSymbolsAndActionsTests.fs +++ /dev/null @@ -1,1022 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -open LspTestFixture - -[] -type LspSymbolsAndActionsTests () = - [] - member _.``Semantic tokens full returns token data`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/semantic-tokens-test.fss" - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create("type User = { Name: string }\nlet user = { Name = \"Ada\" }\nlet x = 42") - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - - LspClient.sendRequest client 17 "textDocument/semanticTokens/full" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 17 with _ -> false - | _ -> false) - - let hasTokenData = - match resp["result"] with - | :? JsonObject as result -> - match result["data"] with - | :? JsonArray as items -> - (items |> Seq.length) >= 5 - | _ -> false - | _ -> false - - hasTokenData |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Semantic tokens classify attribute keyword and module-qualified function`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/semantic-tokens-classification-test.fss" - let source = "[]\nlet values = List.map (fun n -> n + 1) [1]" - let lines = source.Split('\n') - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - - LspClient.sendRequest client 29 "textDocument/semanticTokens/full" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 29 with _ -> false - | _ -> false) - - let decodedTokens = - let mutable previousLine = 0 - let mutable previousStart = 0 - match resp["result"] with - | :? JsonObject as result -> - match result["data"] with - | :? JsonArray as data -> - data - |> Seq.toArray - |> Array.chunkBySize 5 - |> Array.choose (fun chunk -> - if chunk.Length <> 5 then None else - let readInt (n: JsonNode | null) = - match n with - | null -> None - | :? JsonValue as v -> (try Some (v.GetValue()) with _ -> None) - | _ -> None - - match readInt chunk[0], readInt chunk[1], readInt chunk[2], readInt chunk[3] with - | Some deltaLine, Some deltaStart, Some length, Some tokenType -> - let line = previousLine + deltaLine - let start = if deltaLine = 0 then previousStart + deltaStart else deltaStart - previousLine <- line - previousStart <- start - let text = - if line >= 0 && line < lines.Length && start >= 0 && start + length <= lines[line].Length then - lines[line].Substring(start, length) - else - "" - Some (line, start, text, tokenType) - | _ -> None) - | _ -> Array.empty - | _ -> Array.empty - - let hasExportKeyword = - decodedTokens - |> Array.exists (fun (_, _, text, tokenType) -> text = "export" && tokenType = 0) - let hasListMapFunction = - decodedTokens - |> Array.exists (fun (_, _, text, tokenType) -> text = "List.map" && tokenType = 3) - - hasExportKeyword |> should equal true - hasListMapFunction |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``DidOpen publishes diagnostics on parse error`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/diagnostic-test.fss" - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create("let x =") - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diagMsg = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv when (try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false) -> - match msg["params"] with - | :? JsonObject as p -> - match p["uri"], p["diagnostics"] with - | :? JsonValue as u, diagnosticsNode -> - match diagnosticsNode with - | :? JsonArray as diagnosticsArray -> - (try u.GetValue() = uri with _ -> false) - && ((diagnosticsArray |> Seq.length) > 0) - | _ -> false - | _ -> false - | _ -> false - | _ -> false) - - diagMsg |> should not' (equal null) - - let hasExpectedDiagnosticMetadata = - match diagMsg["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as diagnostics -> - diagnostics - |> Seq.exists (fun diag -> - match diag with - | :? JsonObject as d -> - let sourceOk = - match d["source"] with - | :? JsonValue as v -> (try v.GetValue() = "fscript-lsp" with _ -> false) - | _ -> false - let codeOk = - match d["code"] with - | :? JsonValue as v -> (try v.GetValue() = "parse" with _ -> false) - | _ -> false - sourceOk && codeOk - | _ -> false) - | _ -> false - | _ -> false - - hasExpectedDiagnosticMetadata |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``DidOpen does not publish unused binding warning`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/unused-binding-test.fss" - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create("let unused_value = 1\nlet used_value = 2\nused_value") - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diag = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - - let hasUnusedWarning = - match diag["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as items -> - items - |> Seq.exists (fun item -> - match item with - | :? JsonObject as d -> - let codeOk = - match d["code"] with - | :? JsonValue as cv -> (try cv.GetValue() = "unused" with _ -> false) - | _ -> false - let severityOk = - match d["severity"] with - | :? JsonValue as sv -> (try sv.GetValue() = 2 with _ -> false) - | _ -> false - codeOk && severityOk - | _ -> false) - | _ -> false - | _ -> false - - hasUnusedWarning |> should equal false - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``DidOpen does not publish unused warning for exported binding`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/unused-exported-test.fss" - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create("[]\nlet defaults = 42") - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diagMsg = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv when (try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false) -> - match msg["params"] with - | :? JsonObject as p -> - match p["uri"] with - | :? JsonValue as u -> - try u.GetValue() = uri with _ -> false - | _ -> false - | _ -> false - | _ -> false) - - let hasUnusedWarning = - match diagMsg["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as diagnosticsArray -> - diagnosticsArray - |> Seq.exists (fun d -> - match d with - | :? JsonObject as diag -> - let codeMatches = - match diag["code"] with - | :? JsonValue as codeValue -> - try codeValue.GetValue() = "unused" with _ -> false - | _ -> false - let messageMatches = - match diag["message"] with - | :? JsonValue as messageValue -> - try messageValue.GetValue().Contains("defaults", StringComparison.Ordinal) with _ -> false - | _ -> false - codeMatches && messageMatches - | _ -> false) - | _ -> false - | _ -> false - - hasUnusedWarning |> should equal false - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``DidOpen does not publish unused warnings from included files`` () = - let client = LspClient.start () - try - initialize client - - let tempRoot = Path.Combine(Path.GetTempPath(), $"fscript-lsp-unused-include-{Guid.NewGuid():N}") - Directory.CreateDirectory(tempRoot) |> ignore - let includeFile = Path.Combine(tempRoot, "_helpers.fss") - let mainFile = Path.Combine(tempRoot, "main.fss") - File.WriteAllText(includeFile, "let with_flag x = x\n") - File.WriteAllText(mainFile, "import \"_helpers.fss\"\nlet first_project_file x = x\nlet result = first_project_file 1\nresult\n") - - let uri = Uri(mainFile).AbsoluteUri - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(File.ReadAllText(mainFile)) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diagMsg = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv when (try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false) -> - match msg["params"] with - | :? JsonObject as p -> - match p["uri"] with - | :? JsonValue as u -> - try u.GetValue() = uri with _ -> false - | _ -> false - | _ -> false - | _ -> false) - - let hasIncludedUnusedWarning = - match diagMsg["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as diagnosticsArray -> - diagnosticsArray - |> Seq.exists (fun d -> - match d with - | :? JsonObject as diag -> - let codeOk = - match diag["code"] with - | :? JsonValue as cv -> (try cv.GetValue() = "unused" with _ -> false) - | _ -> false - let messageMentionsIncludedBinding = - match diag["message"] with - | :? JsonValue as mv -> - try mv.GetValue().Contains("with_flag", StringComparison.Ordinal) with _ -> false - | _ -> false - codeOk && messageMentionsIncludedBinding - | _ -> false) - | _ -> false - | _ -> false - - hasIncludedUnusedWarning |> should equal false - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``DidOpen does not publish unused warnings for underscore helper files`` () = - let client = LspClient.start () - try - initialize client - - let helperFile = Path.Combine(Path.GetTempPath(), $"_helpers-{Guid.NewGuid():N}.fss") - File.WriteAllText(helperFile, "let append_part part acc = acc\n") - - let uri = Uri(helperFile).AbsoluteUri - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(File.ReadAllText(helperFile)) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diagMsg = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv when (try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false) -> - match msg["params"] with - | :? JsonObject as p -> - match p["uri"] with - | :? JsonValue as u -> - try u.GetValue() = uri with _ -> false - | _ -> false - | _ -> false - | _ -> false) - - let hasUnusedWarning = - match diagMsg["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as diagnosticsArray -> - diagnosticsArray - |> Seq.exists (fun d -> - match d with - | :? JsonObject as diag -> - match diag["code"] with - | :? JsonValue as cv -> (try cv.GetValue() = "unused" with _ -> false) - | _ -> false - | _ -> false) - | _ -> false - | _ -> false - - hasUnusedWarning |> should equal false - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``DidOpen does not report unbound variable for intrinsic print`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/print-intrinsic-test.fss" - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create("print \"hello\"") - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diagMsg = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - - let hasUnboundPrint = - match diagMsg["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as items -> - items - |> Seq.exists (fun item -> - match item with - | :? JsonObject as d -> - match d["message"] with - | :? JsonValue as m -> - try m.GetValue().Contains("Unbound variable 'print'", StringComparison.Ordinal) with _ -> false - | _ -> false - | _ -> false) - | _ -> false - | _ -> false - - hasUnboundPrint |> should equal false - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Code action suggests quick fix for unbound variable typo`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/code-action-typo-test.fss" - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create("let alpha = 1\nalph") - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diagMsg = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - - let diagnostics = - match diagMsg["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as items -> items - | _ -> JsonArray() - | _ -> JsonArray() - - let req = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - req["textDocument"] <- textDocument - let range = JsonObject() - let startPos = JsonObject() - startPos["line"] <- JsonValue.Create(1) - startPos["character"] <- JsonValue.Create(0) - let endPos = JsonObject() - endPos["line"] <- JsonValue.Create(1) - endPos["character"] <- JsonValue.Create(4) - range["start"] <- startPos - range["end"] <- endPos - req["range"] <- range - let context = JsonObject() - let diagnosticsCopy = JsonArray() - for d in diagnostics do - match d with - | null -> () - | node -> - diagnosticsCopy.Add(node.DeepClone()) - context["diagnostics"] <- diagnosticsCopy - req["context"] <- context - - LspClient.sendRequest client 18 "textDocument/codeAction" (Some req) - let resp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 18 with _ -> false - | _ -> false) - - let hasAlphaSuggestion = - match resp["result"] with - | :? JsonArray as items -> - items - |> Seq.exists (fun item -> - match item with - | :? JsonObject as action -> - match action["title"] with - | :? JsonValue as title -> - try title.GetValue().Contains("'alpha'", StringComparison.Ordinal) with _ -> false - | _ -> false - | _ -> false) - | _ -> false - - hasAlphaSuggestion |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Document highlight returns local occurrences for selected symbol`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/document-highlight-test.fss" - let source = "let alpha x = x + 1\nlet v = alpha 41\nalpha v" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let hParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(9) - hParams["textDocument"] <- textDocument - hParams["position"] <- position - - LspClient.sendRequest client 14 "textDocument/documentHighlight" (Some hParams) - let hResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 14 with _ -> false - | _ -> false) - - let count = - match hResp["result"] with - | :? JsonArray as items -> items |> Seq.length - | _ -> 0 - - count |> should be (greaterThanOrEqualTo 3) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Workspace symbol returns matches across opened documents`` () = - let client = LspClient.start () - try - initialize client - - let openDoc (uri: string) (source: string) = - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - openDoc "file:///tmp/workspace-symbol-1.fss" "let alpha x = x + 1" - openDoc "file:///tmp/workspace-symbol-2.fss" "let beta y = y + 2" - - let wsParams = JsonObject() - wsParams["query"] <- JsonValue.Create("alpha") - - LspClient.sendRequest client 15 "workspace/symbol" (Some wsParams) - let wsResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 15 with _ -> false - | _ -> false) - - let hasAlpha = - match wsResp["result"] with - | :? JsonArray as items -> - items - |> Seq.exists (fun item -> - match item with - | :? JsonObject as o -> - match o["name"] with - | :? JsonValue as v -> - try v.GetValue() = "alpha" with _ -> false - | _ -> false - | _ -> false) - | _ -> false - - hasAlpha |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Language server typing includes runtime externs`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/runtime-externs-typing-test.fss" - let source = "let ok = Fs.exists \".\"\nok\n" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - let diagnosticsMsg = - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - - let diagnosticsCount = - match diagnosticsMsg["params"] with - | :? JsonObject as p -> - match p["diagnostics"] with - | :? JsonArray as arr -> arr.Count - | _ -> -1 - | _ -> -1 - - Assert.That(diagnosticsCount, Is.EqualTo(0)) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Rename returns workspace edit for all symbol occurrences`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/rename-test.fss" - let source = "let value = 1\nlet x = value + 2\nvalue" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let renameParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(10) - renameParams["textDocument"] <- textDocument - renameParams["position"] <- position - renameParams["newName"] <- JsonValue.Create("count") - - LspClient.sendRequest client 10 "textDocument/rename" (Some renameParams) - let renameResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 10 with _ -> false - | _ -> false) - - let renameCount = - match renameResp["result"] with - | :? JsonObject as result -> - match result["changes"] with - | :? JsonObject as changes -> - match changes[uri] with - | :? JsonArray as edits -> - edits - |> Seq.filter (fun edit -> - match edit with - | :? JsonObject as o -> - match o["newText"] with - | :? JsonValue as v -> (try v.GetValue() = "count" with _ -> false) - | _ -> false - | _ -> false) - |> Seq.length - | _ -> 0 - | _ -> 0 - | _ -> 0 - - renameCount |> should be (greaterThanOrEqualTo 3) - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Rename returns workspace edits across opened documents`` () = - let client = LspClient.start () - try - initialize client - - let openDoc (uri: string) (source: string) = - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let sourceUri = "file:///tmp/rename-source.fss" - let usageUri = "file:///tmp/rename-usage.fss" - openDoc sourceUri "let value = 1" - openDoc usageUri "let a = value + 2\nvalue" - - let renameParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(usageUri) - let position = JsonObject() - position["line"] <- JsonValue.Create(0) - position["character"] <- JsonValue.Create(9) - renameParams["textDocument"] <- textDocument - renameParams["position"] <- position - renameParams["newName"] <- JsonValue.Create("count") - - LspClient.sendRequest client 26 "textDocument/rename" (Some renameParams) - let renameResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 26 with _ -> false - | _ -> false) - - let changedUris = - match renameResp["result"] with - | :? JsonObject as result -> - match result["changes"] with - | :? JsonObject as changes -> - changes - |> Seq.map (fun kv -> kv.Key) - |> Set.ofSeq - | _ -> Set.empty - | _ -> Set.empty - - changedUris.Contains(sourceUri) |> should equal true - changedUris.Contains(usageUri) |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Rename does not rename record field labels when renaming variable`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/rename-field-label-test.fss" - let source = "let value = 1\nlet recd = { value = value }\nvalue" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let renameParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(2) - position["character"] <- JsonValue.Create(2) - renameParams["textDocument"] <- textDocument - renameParams["position"] <- position - renameParams["newName"] <- JsonValue.Create("count") - - LspClient.sendRequest client 13 "textDocument/rename" (Some renameParams) - let renameResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 13 with _ -> false - | _ -> false) - - let editCount = - match renameResp["result"] with - | :? JsonObject as result -> - match result["changes"] with - | :? JsonObject as changes -> - match changes[uri] with - | :? JsonArray as edits -> edits |> Seq.length - | _ -> 0 - | _ -> 0 - | _ -> 0 - - // declaration + variable usage in record value + final usage - editCount |> should equal 3 - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``Rename rejects invalid identifier target`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/rename-invalid-test.fss" - let source = "let value = 1\nvalue" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let renameParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(2) - renameParams["textDocument"] <- textDocument - renameParams["position"] <- position - renameParams["newName"] <- JsonValue.Create("123bad") - - LspClient.sendRequest client 11 "textDocument/rename" (Some renameParams) - let renameResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 11 with _ -> false - | _ -> false) - - let hasInvalidParamsError = - match renameResp["error"] with - | :? JsonObject as err -> - let codeOk = - match err["code"] with - | :? JsonValue as v -> (try v.GetValue() = -32602 with _ -> false) - | _ -> false - let messageOk = - match err["message"] with - | :? JsonValue as v -> - let msg = v.GetValue() - msg.Contains("Invalid rename target", StringComparison.Ordinal) - | _ -> false - codeOk && messageOk - | _ -> false - - hasInvalidParamsError |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client - [] - member _.``PrepareRename returns placeholder and range for valid symbol`` () = - let client = LspClient.start () - try - initialize client - - let uri = "file:///tmp/prepare-rename-test.fss" - let source = "let total = 1\ntotal" - - let td = JsonObject() - td["uri"] <- JsonValue.Create(uri) - td["languageId"] <- JsonValue.Create("fscript") - td["version"] <- JsonValue.Create(1) - td["text"] <- JsonValue.Create(source) - - let didOpenParams = JsonObject() - didOpenParams["textDocument"] <- td - LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams) - - LspClient.readUntil client 10000 (fun msg -> - match msg["method"] with - | :? JsonValue as mv -> - try mv.GetValue() = "textDocument/publishDiagnostics" with _ -> false - | _ -> false) - |> ignore - - let prepareParams = JsonObject() - let textDocument = JsonObject() - textDocument["uri"] <- JsonValue.Create(uri) - let position = JsonObject() - position["line"] <- JsonValue.Create(1) - position["character"] <- JsonValue.Create(2) - prepareParams["textDocument"] <- textDocument - prepareParams["position"] <- position - - LspClient.sendRequest client 12 "textDocument/prepareRename" (Some prepareParams) - let prepareResp = - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 12 with _ -> false - | _ -> false) - - let hasPlaceholder = - match prepareResp["result"] with - | :? JsonObject as result -> - match result["placeholder"] with - | :? JsonValue as v -> (try v.GetValue() = "total" with _ -> false) - | _ -> false - | _ -> false - - hasPlaceholder |> should equal true - finally - try shutdown client with _ -> () - LspClient.stop client diff --git a/tests/FScript.LanguageServer.Tests/LspTestClient.fs b/tests/FScript.LanguageServer.Tests/LspTestClient.fs deleted file mode 100644 index 28da3b2..0000000 --- a/tests/FScript.LanguageServer.Tests/LspTestClient.fs +++ /dev/null @@ -1,117 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -module internal LspClient = - type Client = - { Process: Process - Input: Stream - Output: Stream } - - let private findRepoRoot () = - let mutable current : DirectoryInfo option = Some (DirectoryInfo(AppContext.BaseDirectory)) - let mutable found: string option = None - - while current.IsSome && found.IsNone do - let directory = current.Value - let candidate = Path.Combine(directory.FullName, "FScript.sln") - if File.Exists(candidate) then - found <- Some directory.FullName - else - current <- Option.ofObj directory.Parent - - found |> Option.defaultWith (fun () -> failwith "Unable to locate repository root from test base directory") - - let private ensureServerDllBuilt = - lazy ( - let root = findRepoRoot () - let serverProject = Path.Combine(root, "src", "FScript.LanguageServer", "FScript.LanguageServer.fsproj") - let serverDll = Path.Combine(root, "src", "FScript.LanguageServer", "bin", "Release", "net10.0", "FScript.LanguageServer.dll") - - let buildPsi = - ProcessStartInfo( - FileName = "dotnet", - Arguments = $"build \"{serverProject}\" -c Release -nologo -v q", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true) - - use buildProc = new Process(StartInfo = buildPsi) - if not (buildProc.Start()) then - failwith "Unable to start dotnet build for language server test setup." - buildProc.WaitForExit() - if buildProc.ExitCode <> 0 || not (File.Exists(serverDll)) then - let out = buildProc.StandardOutput.ReadToEnd() - let err = buildProc.StandardError.ReadToEnd() - failwith $"Failed to build language server test target. stdout: {out}\nstderr: {err}" - - serverDll) - - let start () = - let serverDll = ensureServerDllBuilt.Value - - let psi = - ProcessStartInfo( - FileName = "dotnet", - Arguments = $"\"{serverDll}\"", - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true) - - let proc = new Process(StartInfo = psi) - let started = proc.Start() - if not started then failwith "Unable to start FScript language server process" - - { Process = proc - Input = proc.StandardInput.BaseStream - Output = proc.StandardOutput.BaseStream } - - let stop (client: Client) = - if not client.Process.HasExited then - try - client.Process.Kill(true) - with _ -> () - client.Process.Dispose() - - let sendRequest (client: Client) (id: int) (methodName: string) (parameters: JsonNode option) = - let payload = JsonObject() - payload["jsonrpc"] <- JsonValue.Create("2.0") - payload["id"] <- JsonValue.Create(id) - payload["method"] <- JsonValue.Create(methodName) - payload["params"] <- (parameters |> Option.defaultValue (JsonObject())) - LspWire.writeMessage client.Input (payload.ToJsonString()) - - let sendNotification (client: Client) (methodName: string) (parameters: JsonNode option) = - let payload = JsonObject() - payload["jsonrpc"] <- JsonValue.Create("2.0") - payload["method"] <- JsonValue.Create(methodName) - payload["params"] <- (parameters |> Option.defaultValue (JsonObject())) - LspWire.writeMessage client.Input (payload.ToJsonString()) - - let readUntil (client: Client) (timeoutMs: int) (predicate: JsonObject -> bool) = - let deadline = DateTime.UtcNow.AddMilliseconds(float timeoutMs) - let mutable found: JsonObject option = None - - while found.IsNone && DateTime.UtcNow < deadline do - let remaining = int (deadline - DateTime.UtcNow).TotalMilliseconds - if remaining <= 0 then - () - else - let raw = LspWire.readMessageWithTimeout client.Output remaining - let node = JsonNode.Parse(raw) - match node with - | :? JsonObject as obj when predicate obj -> - found <- Some obj - | _ -> () - - found |> Option.defaultWith (fun () -> failwith "Timed out waiting for expected LSP message") diff --git a/tests/FScript.LanguageServer.Tests/LspTestFixture.cs b/tests/FScript.LanguageServer.Tests/LspTestFixture.cs new file mode 100644 index 0000000..a1cd797 --- /dev/null +++ b/tests/FScript.LanguageServer.Tests/LspTestFixture.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Nodes; +using NUnit.Framework; + +namespace FScript.LanguageServer.Tests; + +internal static class LspTestFixture +{ + public static void InitializeWith(LspClient.Client client, JsonObject? initializationOptions) + { + var initializeParams = new JsonObject + { + ["processId"] = null, + ["rootUri"] = null, + ["capabilities"] = new JsonObject() + }; + if (initializationOptions is not null) + { + initializeParams["initializationOptions"] = initializationOptions; + } + + LspClient.SendRequest(client, 1, "initialize", initializeParams); + var response = LspClient.ReadUntil(client, 20_000, msg => msg["id"] is JsonValue idv && idv.TryGetValue(out var id) && id == 1); + Assert.That(response["result"], Is.Not.Null); + LspClient.SendNotification(client, "initialized", null); + } + + public static void Initialize(LspClient.Client client) => InitializeWith(client, null); + + public static void Shutdown(LspClient.Client client) + { + LspClient.SendRequest(client, 2, "shutdown", null); + _ = LspClient.ReadUntil(client, 10_000, msg => msg["id"] is JsonValue idv && idv.TryGetValue(out var id) && id == 2); + LspClient.SendNotification(client, "exit", null); + } +} diff --git a/tests/FScript.LanguageServer.Tests/LspTestFixture.fs b/tests/FScript.LanguageServer.Tests/LspTestFixture.fs deleted file mode 100644 index 53c2328..0000000 --- a/tests/FScript.LanguageServer.Tests/LspTestFixture.fs +++ /dev/null @@ -1,46 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -module internal LspTestFixture = - let initializeWith (client: LspClient.Client) (initializationOptions: JsonObject option) = - let initializeParams = JsonObject() - initializeParams["processId"] <- JsonValue.Create(None) - initializeParams["rootUri"] <- JsonValue.Create(None) - initializeParams["capabilities"] <- JsonObject() - match initializationOptions with - | Some options -> initializeParams["initializationOptions"] <- options - | None -> () - - LspClient.sendRequest client 1 "initialize" (Some initializeParams) - - let response = - LspClient.readUntil client 20000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 1 with _ -> false - | _ -> false) - - response["result"] |> should not' (equal null) - LspClient.sendNotification client "initialized" None - - let initialize (client: LspClient.Client) = - initializeWith client None - - let shutdown (client: LspClient.Client) = - LspClient.sendRequest client 2 "shutdown" None - LspClient.readUntil client 10000 (fun msg -> - match msg["id"] with - | :? JsonValue as idv -> - try idv.GetValue() = 2 with _ -> false - | _ -> false) - |> ignore - - LspClient.sendNotification client "exit" None diff --git a/tests/FScript.LanguageServer.Tests/LspTestWire.fs b/tests/FScript.LanguageServer.Tests/LspTestWire.fs deleted file mode 100644 index 664650b..0000000 --- a/tests/FScript.LanguageServer.Tests/LspTestWire.fs +++ /dev/null @@ -1,92 +0,0 @@ -namespace FScript.LanguageServer.Tests - -open System -open System.IO -open System.Text -open System.Text.Json -open System.Text.Json.Nodes -open System.Diagnostics -open System.Threading -open NUnit.Framework -open FsUnit -module internal LspWire = - let private utf8 = UTF8Encoding(false) - let mutable private pending = Array.empty - - let private readExactWithTimeout (stream: Stream) (buffer: byte[]) (offset: int) (count: int) (timeoutMs: int) = - use cts = new CancellationTokenSource(timeoutMs) - let mutable readTotal = 0 - while readTotal < count do - let read = - stream.ReadAsync(buffer.AsMemory(offset + readTotal, count - readTotal), cts.Token) - |> fun t -> t.GetAwaiter().GetResult() - - if read <= 0 then - failwith "Unexpected end of stream while reading LSP message." - - readTotal <- readTotal + read - - let readMessageWithTimeout (stream: Stream) (timeoutMs: int) : string = - use cts = new CancellationTokenSource(timeoutMs) - let headerBytes = ResizeArray() - let one = Array.zeroCreate 1 - let marker = [| byte '\r'; byte '\n'; byte '\r'; byte '\n' |] - let mutable matched = 0 - let mutable doneHeader = false - - if pending.Length > 0 then - for b in pending do - headerBytes.Add(b) - pending <- Array.empty - - while not doneHeader do - if headerBytes.Count >= marker.Length then - let tail = - [| headerBytes[headerBytes.Count - 4] - headerBytes[headerBytes.Count - 3] - headerBytes[headerBytes.Count - 2] - headerBytes[headerBytes.Count - 1] |] - if tail = marker then - doneHeader <- true - else - let n = stream.ReadAsync(one.AsMemory(0, 1), cts.Token).GetAwaiter().GetResult() - if n <= 0 then failwith "Unexpected end of stream while reading LSP headers." - let b = one[0] - headerBytes.Add(b) - if b = marker[matched] then - matched <- matched + 1 - if matched = marker.Length then doneHeader <- true - else - matched <- if b = marker[0] then 1 else 0 - else - let n = stream.ReadAsync(one.AsMemory(0, 1), cts.Token).GetAwaiter().GetResult() - if n <= 0 then failwith "Unexpected end of stream while reading LSP headers." - let b = one[0] - headerBytes.Add(b) - if b = marker[matched] then - matched <- matched + 1 - if matched = marker.Length then doneHeader <- true - else - matched <- if b = marker[0] then 1 else 0 - - let header = Encoding.ASCII.GetString(headerBytes.ToArray()) - let contentLength = - header.Split([| "\r\n" |], StringSplitOptions.RemoveEmptyEntries) - |> Array.tryPick (fun line -> - if line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase) then - Some (line.Substring("Content-Length:".Length).Trim() |> int) - else - None) - |> Option.defaultWith (fun () -> failwith "Missing Content-Length header") - - let payload = Array.zeroCreate contentLength - readExactWithTimeout stream payload 0 contentLength timeoutMs - utf8.GetString(payload) - - let writeMessage (stream: Stream) (payload: string) = - let payloadBytes = utf8.GetBytes(payload) - let header = $"Content-Length: {payloadBytes.Length}\r\n\r\n" - let headerBytes = Encoding.ASCII.GetBytes(header) - stream.Write(headerBytes, 0, headerBytes.Length) - stream.Write(payloadBytes, 0, payloadBytes.Length) - stream.Flush() diff --git a/tests/FScript.LanguageServer.Tests/LspWire.cs b/tests/FScript.LanguageServer.Tests/LspWire.cs new file mode 100644 index 0000000..805a1bb --- /dev/null +++ b/tests/FScript.LanguageServer.Tests/LspWire.cs @@ -0,0 +1,109 @@ +using System.Text; + +namespace FScript.LanguageServer.Tests; + +internal static class LspWire +{ + private static readonly Encoding Utf8 = new UTF8Encoding(false); + private static byte[] _pending = Array.Empty(); + + private static void ReadExactWithTimeout(Stream stream, byte[] buffer, int offset, int count, int timeoutMs) + { + using var cts = new CancellationTokenSource(timeoutMs); + var readTotal = 0; + while (readTotal < count) + { + var read = stream.ReadAsync(buffer.AsMemory(offset + readTotal, count - readTotal), cts.Token) + .GetAwaiter() + .GetResult(); + if (read <= 0) + { + throw new Exception("Unexpected end of stream while reading LSP message."); + } + + readTotal += read; + } + } + + public static string ReadMessageWithTimeout(Stream stream, int timeoutMs) + { + using var cts = new CancellationTokenSource(timeoutMs); + var headerBytes = new List(); + var one = new byte[1]; + var marker = new[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' }; + var matched = 0; + var doneHeader = false; + + if (_pending.Length > 0) + { + headerBytes.AddRange(_pending); + _pending = Array.Empty(); + } + + while (!doneHeader) + { + if (headerBytes.Count >= marker.Length) + { + var c = headerBytes.Count; + var tail = new[] { headerBytes[c - 4], headerBytes[c - 3], headerBytes[c - 2], headerBytes[c - 1] }; + if (tail.SequenceEqual(marker)) + { + doneHeader = true; + continue; + } + } + + var n = stream.ReadAsync(one.AsMemory(0, 1), cts.Token).GetAwaiter().GetResult(); + if (n <= 0) + { + throw new Exception("Unexpected end of stream while reading LSP headers."); + } + + var b = one[0]; + headerBytes.Add(b); + if (b == marker[matched]) + { + matched++; + if (matched == marker.Length) + { + doneHeader = true; + } + } + else + { + matched = b == marker[0] ? 1 : 0; + } + } + + var header = Encoding.ASCII.GetString(headerBytes.ToArray()); + var contentLength = header.Split(["\r\n"], StringSplitOptions.RemoveEmptyEntries) + .Select(line => + { + if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) + { + return int.Parse(line["Content-Length:".Length..].Trim()); + } + + return -1; + }) + .FirstOrDefault(x => x >= 0); + if (contentLength < 0) + { + throw new Exception("Missing Content-Length header"); + } + + var payload = new byte[contentLength]; + ReadExactWithTimeout(stream, payload, 0, contentLength, timeoutMs); + return Utf8.GetString(payload); + } + + public static void WriteMessage(Stream stream, string payload) + { + var payloadBytes = Utf8.GetBytes(payload); + var header = $"Content-Length: {payloadBytes.Length}\r\n\r\n"; + var headerBytes = Encoding.ASCII.GetBytes(header); + stream.Write(headerBytes, 0, headerBytes.Length); + stream.Write(payloadBytes, 0, payloadBytes.Length); + stream.Flush(); + } +} diff --git a/tests/FScript.LanguageServer.Tests/Program.fs b/tests/FScript.LanguageServer.Tests/Program.fs deleted file mode 100644 index 9ee7eaf..0000000 --- a/tests/FScript.LanguageServer.Tests/Program.fs +++ /dev/null @@ -1,4 +0,0 @@ -module Program - -[] -let main _ = 0 diff --git a/vscode-fscript/README.md b/vscode-fscript/README.md index 712b3f8..86e7042 100644 --- a/vscode-fscript/README.md +++ b/vscode-fscript/README.md @@ -54,4 +54,4 @@ The extension starts the language server with one of these strategies: 1. Custom path from `fscript.server.path` (if configured) 2. Packaged server: `server/FScript.LanguageServer.dll` -3. Local development fallback: builds `../src/FScript.LanguageServer/FScript.LanguageServer.fsproj` and runs `bin/Debug/net10.0/FScript.LanguageServer.dll` +3. Local development fallback: builds `../src/FScript.LanguageServer/FScript.LanguageServer.csproj` and runs `bin/Debug/net10.0/FScript.LanguageServer.dll` diff --git a/vscode-fscript/extension.js b/vscode-fscript/extension.js index 6cf443f..cab264b 100644 --- a/vscode-fscript/extension.js +++ b/vscode-fscript/extension.js @@ -92,7 +92,13 @@ async function createServerOptions(context, config) { return null; } - const projectPath = path.resolve(context.extensionPath, '..', 'src', 'FScript.LanguageServer', 'FScript.LanguageServer.fsproj'); + const projectPath = path.resolve( + context.extensionPath, + '..', + 'src', + 'FScript.LanguageServer', + 'FScript.LanguageServer.csproj' + ); const outputDll = path.resolve( context.extensionPath, '..',