Skip to content

Commit f1eb936

Browse files
authored
Merge pull request #1 from MagnusOpera/feature/langserver-rewrite-csharp
feat: langserver rewrite in C#
2 parents c99ab5c + 7d0243c commit f1eb936

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1350
-4825
lines changed

.github/workflows/ci-pr.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ on:
44
pull_request:
55
branches:
66
- main
7+
types:
8+
- opened
9+
- reopened
10+
- synchronize
11+
- ready_for_review
712

813
permissions:
914
contents: read

.github/workflows/on-push-tag.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
shell: bash
4646
run: |
4747
rm -rf vscode-fscript/server
48-
dotnet publish src/FScript.LanguageServer/FScript.LanguageServer.fsproj -c Release -p:PublishAot=false -o vscode-fscript/server
48+
dotnet publish src/FScript.LanguageServer/FScript.LanguageServer.csproj -c Release -p:PublishAot=false -o vscode-fscript/server
4949
5050
- name: Install extension packaging tool
5151
run: npm install -g @vscode/vsce@3.5

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ All notable changes to FScript are documented in this file.
44

55
## [Unreleased]
66

7+
- Removed F# sources from `src/FScript.LanguageServer*` by moving LSP semantic modules into `FScript.CSharpInterop` and keeping `FScript.LanguageServer` as C# host.
8+
- Replaced `FScript.LanguageServer.Tests` project with a C# test project and C# LSP test harness to remove F# compile cost from LanguageServer test builds.
9+
- Deleted obsolete F# LanguageServer test sources after C# test project migration.
10+
- Renamed `FScript.CSharpInterop/LanguageServerLegacy` to `FScript.CSharpInterop/LanguageServer` to reflect the new primary architecture.
11+
- CI now runs branch update builds on PR `synchronize` events while keeping `ci-main` scoped to `main` pushes to avoid duplicate runs.
712
- Enabled F# preview parallel compilation globally, disabled deterministic builds, and removed global RuntimeIdentifiers to reduce CI build latency.
13+
- Added `FScript.CSharpInterop` as a stable bridge for parse/infer/runtime-extern/stdlib-source services and wired LanguageServer through it.
14+
- Added `FScript.LanguageServer` host executable as the migration entrypoint for C#-owned LSP startup.
15+
- Added a first native C# LSP server core (JSON-RPC transport, initialize/shutdown, text sync, and stdlib-source request) with dedicated integration tests.
16+
- Extended the native C# LSP core with diagnostics publishing and `viewAst`/`viewInferredAst` command handling.
17+
- 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`.
18+
- Replaced the F# LSP server executable with `FScript.LanguageServer` (C#) and moved F# LSP logic into `FScript.LanguageServer.Core`.
19+
- Fixed imported qualified type annotations (for example `common.ProjectInfo`) in parser/type inference to prevent false type mismatches.
820

921
## [0.33.0]
1022

FScript.sln

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.Runtime", "src\FScr
1717
EndProject
1818
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.Runtime.Tests", "tests\FScript.Runtime.Tests\FScript.Runtime.Tests.fsproj", "{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}"
1919
EndProject
20-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.LanguageServer", "src\FScript.LanguageServer\FScript.LanguageServer.fsproj", "{E22A34B5-F5E8-422D-9BA5-932B3C45188F}"
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FScript.LanguageServer.Tests", "tests\FScript.LanguageServer.Tests\FScript.LanguageServer.Tests.csproj", "{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}"
2121
EndProject
22-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.LanguageServer.Tests", "tests\FScript.LanguageServer.Tests\FScript.LanguageServer.Tests.fsproj", "{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}"
22+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FScript.CSharpInterop", "src\FScript.CSharpInterop\FScript.CSharpInterop.fsproj", "{8A28B784-F90B-469C-91BE-F96F63ACEA32}"
23+
EndProject
24+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FScript.LanguageServer", "src\FScript.LanguageServer\FScript.LanguageServer.csproj", "{57518676-01F0-4D5B-A53B-7A06DBA9AA04}"
2325
EndProject
2426
Global
2527
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -91,18 +93,6 @@ Global
9193
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x64.Build.0 = Release|Any CPU
9294
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x86.ActiveCfg = Release|Any CPU
9395
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7}.Release|x86.Build.0 = Release|Any CPU
94-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
95-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|Any CPU.Build.0 = Debug|Any CPU
96-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x64.ActiveCfg = Debug|Any CPU
97-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x64.Build.0 = Debug|Any CPU
98-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x86.ActiveCfg = Debug|Any CPU
99-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Debug|x86.Build.0 = Debug|Any CPU
100-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|Any CPU.ActiveCfg = Release|Any CPU
101-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|Any CPU.Build.0 = Release|Any CPU
102-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x64.ActiveCfg = Release|Any CPU
103-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x64.Build.0 = Release|Any CPU
104-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x86.ActiveCfg = Release|Any CPU
105-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F}.Release|x86.Build.0 = Release|Any CPU
10696
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10797
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
10898
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -115,6 +105,30 @@ Global
115105
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x64.Build.0 = Release|Any CPU
116106
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x86.ActiveCfg = Release|Any CPU
117107
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1}.Release|x86.Build.0 = Release|Any CPU
108+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
109+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|Any CPU.Build.0 = Debug|Any CPU
110+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x64.ActiveCfg = Debug|Any CPU
111+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x64.Build.0 = Debug|Any CPU
112+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x86.ActiveCfg = Debug|Any CPU
113+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Debug|x86.Build.0 = Debug|Any CPU
114+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|Any CPU.ActiveCfg = Release|Any CPU
115+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|Any CPU.Build.0 = Release|Any CPU
116+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x64.ActiveCfg = Release|Any CPU
117+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x64.Build.0 = Release|Any CPU
118+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x86.ActiveCfg = Release|Any CPU
119+
{8A28B784-F90B-469C-91BE-F96F63ACEA32}.Release|x86.Build.0 = Release|Any CPU
120+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
121+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|Any CPU.Build.0 = Debug|Any CPU
122+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x64.ActiveCfg = Debug|Any CPU
123+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x64.Build.0 = Debug|Any CPU
124+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x86.ActiveCfg = Debug|Any CPU
125+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Debug|x86.Build.0 = Debug|Any CPU
126+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|Any CPU.ActiveCfg = Release|Any CPU
127+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|Any CPU.Build.0 = Release|Any CPU
128+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x64.ActiveCfg = Release|Any CPU
129+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x64.Build.0 = Release|Any CPU
130+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x86.ActiveCfg = Release|Any CPU
131+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04}.Release|x86.Build.0 = Release|Any CPU
118132
EndGlobalSection
119133
GlobalSection(SolutionProperties) = preSolution
120134
HideSolutionNode = FALSE
@@ -125,7 +139,8 @@ Global
125139
{9C62883E-EFB0-4D9E-84F3-4138C123F55E} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
126140
{8C2C5767-857A-44B0-80C2-DC90E0A60F4D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
127141
{1E2C7B34-04B8-42C9-880D-CC47DEC156A7} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
128-
{E22A34B5-F5E8-422D-9BA5-932B3C45188F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
129142
{B734E1E1-59C2-47E0-8D19-A9C5C95938F1} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
143+
{8A28B784-F90B-469C-91BE-F96F63ACEA32} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
144+
{57518676-01F0-4D5B-A53B-7A06DBA9AA04} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
130145
EndGlobalSection
131146
EndGlobal

docs/architecture/assemblies-and-roles.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,32 @@ Use this when:
5555
NuGet:
5656
- `MagnusOpera.FScript.Runtime`
5757

58+
### `FScript.CSharpInterop`
59+
Role:
60+
- C#-friendly integration facade over language + runtime services.
61+
62+
Responsibilities:
63+
- Resolve runtime extern catalog from source path/root context.
64+
- Parse with include/import expansion through a stable interop entry point.
65+
- Run inference APIs through a single host-facing surface.
66+
- Expose stdlib virtual source loading for editor integrations.
67+
68+
Use this when:
69+
- You integrate FScript from C# and want to avoid direct F# compiler/runtime internals.
70+
- You build tooling services (for example LSP hosts) with a stable boundary.
71+
72+
### `FScript.LanguageServer`
73+
Role:
74+
- C# host executable for the Language Server process.
75+
76+
Responsibilities:
77+
- Provide the production C# process host for LSP startup/dispatch.
78+
- Execute the full LSP method surface used by the VS Code extension.
79+
- Keep protocol behavior aligned with existing language/runtime analysis services.
80+
81+
Use this when:
82+
- You want C# ownership of the server host process while reusing existing language services.
83+
5884
## Typical composition
5985

6086
### CLI execution path
@@ -72,6 +98,8 @@ NuGet:
7298
## Dependency direction
7399
- `FScript.Language` has no dependency on `FScript.Runtime`.
74100
- `FScript.Runtime` depends on `FScript.Language` types.
101+
- `FScript.CSharpInterop` depends on both `FScript.Language` and `FScript.Runtime`.
102+
- `FScript.LanguageServer` depends on `FScript.CSharpInterop`.
75103
- `FScript` depends on both `FScript.Language` and `FScript.Runtime`.
76104

77105
This keeps the language engine reusable while runtime capabilities remain host-configurable.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<Compile Include="InteropServices.fs" />
10+
<Compile Include="LanguageServer\LspProtocol.fs" />
11+
<Compile Include="LanguageServer\LspModel.fs" />
12+
<Compile Include="LanguageServer\LspRuntimeExterns.fs" />
13+
<Compile Include="LanguageServer\AstJson.fs" />
14+
<Compile Include="LanguageServer\LspSymbols.fs" />
15+
<Compile Include="LanguageServer\LspHandlers.fs" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\FScript.Language\FScript.Language.fsproj" />
20+
<ProjectReference Include="..\FScript.Runtime\FScript.Runtime.fsproj" />
21+
</ItemGroup>
22+
23+
</Project>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
namespace FScript.CSharpInterop
2+
3+
open System
4+
open System.IO
5+
open FScript.Language
6+
open FScript.Runtime
7+
8+
module InteropServices =
9+
let private resolveRootDirectory (sourcePath: string) =
10+
try
11+
match Path.GetDirectoryName(sourcePath) with
12+
| null
13+
| "" -> Directory.GetCurrentDirectory()
14+
| dir -> dir
15+
with _ ->
16+
Directory.GetCurrentDirectory()
17+
18+
let runtimeExternsForSourcePath (sourcePath: string) : ExternalFunction list =
19+
let ctx = { HostContext.RootDirectory = resolveRootDirectory sourcePath }
20+
Registry.all ctx
21+
22+
let parseProgramFromSourceWithIncludes (sourcePath: string) (sourceText: string) : Program =
23+
let rootDirectory = resolveRootDirectory sourcePath
24+
IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText
25+
26+
let inferProgramWithExterns (externs: ExternalFunction list) (program: Program) : TypeInfer.TypedProgram =
27+
TypeInfer.inferProgramWithExterns externs program
28+
29+
let inferProgramWithExternsAndLocalVariableTypes (externs: ExternalFunction list) (program: Program) : TypeInfer.TypedProgram * TypeInfer.LocalVariableTypeInfo list =
30+
TypeInfer.inferProgramWithExternsAndLocalVariableTypes externs program
31+
32+
let inferStdlibWithExternsRaw (externs: ExternalFunction list) : TypeInfer.TypedProgram =
33+
TypeInfer.inferProgramWithExternsRaw externs (Stdlib.loadProgram())
34+
35+
let stdlibProgram () : Program =
36+
Stdlib.loadProgram()
37+
38+
let tryLoadStdlibSourceText (uri: string) : string option =
39+
try
40+
let parsed = Uri(uri)
41+
if not (String.Equals(parsed.Scheme, "fscript-stdlib", StringComparison.OrdinalIgnoreCase)) then
42+
None
43+
else
44+
let fileName = parsed.AbsolutePath.TrimStart('/')
45+
let resourceName =
46+
match fileName with
47+
| "Option.fss" -> Some "FScript.Language.Stdlib.Option.fss"
48+
| "List.fss" -> Some "FScript.Language.Stdlib.List.fss"
49+
| "Map.fss" -> Some "FScript.Language.Stdlib.Map.fss"
50+
| _ -> None
51+
52+
match resourceName with
53+
| None -> None
54+
| Some name ->
55+
let assembly = typeof<Span>.Assembly
56+
match assembly.GetManifestResourceStream(name) with
57+
| null -> None
58+
| stream ->
59+
use stream = stream
60+
use reader = new StreamReader(stream)
61+
Some(reader.ReadToEnd())
62+
with _ ->
63+
None
File renamed without changes.

src/FScript.LanguageServer/LspHandlers.fs renamed to src/FScript.CSharpInterop/LanguageServer/LspHandlers.fs

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ open System
44
open System.IO
55
open System.Text.Json.Nodes
66
open FScript.Language
7+
open FScript.CSharpInterop
78

89
module LspHandlers =
910
open LspModel
@@ -497,12 +498,7 @@ module LspHandlers =
497498
| None ->
498499
sendCommandError idNode "internal" $"Unable to read source file '{sourcePath}'."
499500
| Some sourceText ->
500-
let rootDirectory =
501-
match Path.GetDirectoryName(sourcePath) with
502-
| null
503-
| "" -> "."
504-
| dir -> dir
505-
let program = IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText
501+
let program = InteropServices.parseProgramFromSourceWithIncludes sourcePath sourceText
506502
let response = JsonObject()
507503
response["ok"] <- JsonValue.Create(true)
508504
response["data"] <- AstJson.programToJson sourcePath program
@@ -528,14 +524,9 @@ module LspHandlers =
528524
| None ->
529525
sendCommandError idNode "internal" $"Unable to read source file '{sourcePath}'."
530526
| Some sourceText ->
531-
let rootDirectory =
532-
match Path.GetDirectoryName(sourcePath) with
533-
| null
534-
| "" -> "."
535-
| dir -> dir
536-
let program = IncludeResolver.parseProgramFromSourceWithIncludes rootDirectory sourcePath sourceText
527+
let program = InteropServices.parseProgramFromSourceWithIncludes sourcePath sourceText
537528
let runtimeExterns = LspRuntimeExterns.forSourcePath sourcePath
538-
let typedProgram = TypeInfer.inferProgramWithExterns runtimeExterns program
529+
let typedProgram = InteropServices.inferProgramWithExterns runtimeExterns program
539530
let response = JsonObject()
540531
response["ok"] <- JsonValue.Create(true)
541532
response["data"] <- AstJson.typedProgramToJson sourcePath typedProgram
@@ -1103,40 +1094,12 @@ module LspHandlers =
11031094
| _ ->
11041095
LspProtocol.sendResponse idNode None
11051096

1106-
let private tryLoadStdlibSourceText (uri: string) =
1107-
try
1108-
let parsed = Uri(uri)
1109-
if not (String.Equals(parsed.Scheme, "fscript-stdlib", StringComparison.OrdinalIgnoreCase)) then
1110-
None
1111-
else
1112-
let fileName = parsed.AbsolutePath.TrimStart('/')
1113-
let resourceName =
1114-
match fileName with
1115-
| "Option.fss" -> Some "FScript.Language.Stdlib.Option.fss"
1116-
| "List.fss" -> Some "FScript.Language.Stdlib.List.fss"
1117-
| "Map.fss" -> Some "FScript.Language.Stdlib.Map.fss"
1118-
| _ -> None
1119-
1120-
match resourceName with
1121-
| None -> None
1122-
| Some name ->
1123-
let assembly = typeof<Span>.Assembly
1124-
match assembly.GetManifestResourceStream(name) with
1125-
| null ->
1126-
None
1127-
| stream ->
1128-
use stream = stream
1129-
use reader = new StreamReader(stream)
1130-
Some (reader.ReadToEnd())
1131-
with _ ->
1132-
None
1133-
11341097
let handleStdlibSource (idNode: JsonNode) (paramsObj: JsonObject) =
11351098
match tryGetString paramsObj "uri" with
11361099
| None ->
11371100
sendCommandError idNode "internal" "Missing stdlib URI."
11381101
| Some uri ->
1139-
match tryLoadStdlibSourceText uri with
1102+
match InteropServices.tryLoadStdlibSourceText uri with
11401103
| Some sourceText ->
11411104
let response = JsonObject()
11421105
response["ok"] <- JsonValue.Create(true)
File renamed without changes.

0 commit comments

Comments
 (0)