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