diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index fb52ee41..d983725c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,8 +3,8 @@ FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-6.0-focal # Install other dotnet versions (required for adr tool and dotnet suggest) RUN curl -sSL https://dot.net/v1/dotnet-install.sh > /tmp/dotnet-install.sh \ && chmod u+x /tmp/dotnet-install.sh -RUN sudo bash -s -- --install-dir /usr/share/dotnet --channel 3.1 --runtime dotnet -RUN sudo bash -s -- --install-dir /usr/share/dotnet --channel 3.1 --runtime dotnet +RUN sudo /tmp/dotnet-install.sh --install-dir /usr/share/dotnet --channel 5.0 --runtime dotnet +RUN sudo /tmp/dotnet-install.sh --install-dir /usr/share/dotnet --channel 3.1 --runtime dotnet # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="lts/*" diff --git a/.github/workflows/language-server-build.yml b/.github/workflows/language-server-build.yml index aa00d85f..2cc242ca 100644 --- a/.github/workflows/language-server-build.yml +++ b/.github/workflows/language-server-build.yml @@ -28,7 +28,7 @@ jobs: working-directory: ./src - name: Build and Test Language Server run: | - dotnet run --project ./src/Ubictionary.LanguageServer.Tests/Ubictionary.LanguageServer.Tests.fsproj -- --nunit-summary TestResults-${{ matrix.dotnet-version }}.xml + dotnet run --project ./src/Ubictionary.LanguageServer.Tests/Ubictionary.LanguageServer.Tests.fsproj -- --fail-on-focused-tests --nunit-summary TestResults-${{ matrix.dotnet-version }}.xml - name: Upload dotnet test results uses: actions/upload-artifact@v2 with: diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a9a569f9..e0c7a121 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -38,6 +38,20 @@ }, "group": "build", "problemMatcher": "$msCompile" + }, + { + "label": "watch tests", + "command": "dotnet", + "type": "shell", + "args": [ + "watch", + "run" + ], + "options": { + "cwd": "${workspaceFolder}/src/Ubictionary.LanguageServer.Tests" + }, + "group": "test", + "problemMatcher": "$msCompile" } ] } \ No newline at end of file diff --git a/src/Ubictionary.LanguageServer.Tests/ConditionAwaiter.fs b/src/Ubictionary.LanguageServer.Tests/ConditionAwaiter.fs new file mode 100644 index 00000000..74805105 --- /dev/null +++ b/src/Ubictionary.LanguageServer.Tests/ConditionAwaiter.fs @@ -0,0 +1,31 @@ +module Ubictionary.LanguageServer.Tests.ConditionAwaiter + +type WaitForCondition<'T> = + { + ReplyChannel: AsyncReplyChannel<'T> + Condition: 'T -> bool + } + +type Message<'T> = + | Received of 'T + | WaitFor of WaitForCondition<'T> + + +let create<'T>() = MailboxProcessor.Start(fun inbox -> + let rec loop (conditions: WaitForCondition<'T> list) = async { + let! (msg: Message<'T>) = inbox.Receive() + let newState = + match msg with + | Received msg -> + conditions |> Seq.iter (fun c -> if c.Condition msg then c.ReplyChannel.Reply(msg)) + conditions + | WaitFor waitFor -> waitFor :: conditions + return! loop newState + } + loop []) + +let received (awaiter: MailboxProcessor>) msg = + awaiter.Post(Received(msg)) + +let waitFor (awaiter: MailboxProcessor>) condition timeout = + awaiter.PostAndTryAsyncReply((fun rc -> WaitFor({ReplyChannel=rc;Condition=condition})), timeout) \ No newline at end of file diff --git a/src/Ubictionary.LanguageServer.Tests/ConfigurationSection.fs b/src/Ubictionary.LanguageServer.Tests/ConfigurationSection.fs new file mode 100644 index 00000000..ae188ac6 --- /dev/null +++ b/src/Ubictionary.LanguageServer.Tests/ConfigurationSection.fs @@ -0,0 +1,16 @@ +module Ubictionary.LanguageServer.Tests.ConfigurationSection + +open Newtonsoft.Json.Linq +open System.Collections.Generic +open OmniSharp.Extensions.LanguageServer.Protocol.Models + +let fromMap values = + let results = new List() + let configValue = JObject() + results.Add(configValue) + values |> Map.iter (fun k (v:string) -> + configValue.[k] <- JValue(v)) + Container(results) + +let includesSection section (configRequest:ConfigurationParams) = + configRequest.Items |> Seq.map (fun ci -> ci.Section) |> Seq.contains section \ No newline at end of file diff --git a/src/Ubictionary.LanguageServer.Tests/InitializationTests.fs b/src/Ubictionary.LanguageServer.Tests/InitializationTests.fs index ba048c33..0338daaa 100644 --- a/src/Ubictionary.LanguageServer.Tests/InitializationTests.fs +++ b/src/Ubictionary.LanguageServer.Tests/InitializationTests.fs @@ -1,12 +1,32 @@ module Ubictionary.LanguageServer.Tests.InitializationTests + +open System +open System.Threading.Tasks open Expecto open Swensen.Unquote +open OmniSharp.Extensions.JsonRpc +open OmniSharp.Extensions.LanguageServer.Protocol.Models +open OmniSharp.Extensions.LanguageServer.Protocol.Client +open OmniSharp.Extensions.LanguageServer.Client + +let private initTestClient = async { + let testClient = new TestClient() + return! testClient.Initialize(None) |> Async.AwaitTask +} -let initTestClient = async { +let private initTestClientWithConfig clientConfigBuilder = async { let testClient = new TestClient() - return! testClient.Initialize() |> Async.AwaitTask + return! testClient.Initialize(Some clientConfigBuilder) |> Async.AwaitTask } +let private handleConfigurationRequest section configValues = + let configSectionResult = ConfigurationSection.fromMap configValues + fun c -> + if ConfigurationSection.includesSection section c then + Task.FromResult(configSectionResult) + else + Task.FromResult(null) + [] let initializationTests = testList "Initialization Tests" [ @@ -20,7 +40,35 @@ let initializationTests = testAsync "Has Correct ServerInfo Name" { use! client = initTestClient test <@ client.ServerSettings.ServerInfo.Name = "Ubictionary" @> - } + } + + testAsync "Server requests ubictionary file location configuration" { + let pathValue = Guid.NewGuid().ToString() + + let configHandler = handleConfigurationRequest "ubictionary" (Map [("ubictionary_path", pathValue)]) + + let logAwaiter = ConditionAwaiter.create() + let logHandler (l:LogMessageParams) = + l.Message |> ConditionAwaiter.received logAwaiter + Task.CompletedTask + + let clientOptionsBuilder (b:LanguageClientOptions) = + b.OnRequest("workspace/configuration", configHandler, JsonRpcHandlerOptions()) + .OnNotification("window/logMessage", logHandler, JsonRpcHandlerOptions()) + .WithCapability(Capabilities.DidChangeConfigurationCapability()) + |> ignore + + use! client = clientOptionsBuilder |> initTestClientWithConfig + + test <@ client.ClientSettings.Capabilities.Workspace.Configuration.IsSupported @> + test <@ client.ClientSettings.Capabilities.Workspace.DidChangeConfiguration.IsSupported @> + + let logCondition = fun (m:string) -> m.Contains("Loading ubictionary") + let! reply = ConditionAwaiter.waitFor logAwaiter logCondition 1500 + test <@ match reply with + | Some replyMsg -> replyMsg.Contains(pathValue) + | None -> false @> + } ] \ No newline at end of file diff --git a/src/Ubictionary.LanguageServer.Tests/TestClient.fs b/src/Ubictionary.LanguageServer.Tests/TestClient.fs index d0a01167..1c0b90e7 100644 --- a/src/Ubictionary.LanguageServer.Tests/TestClient.fs +++ b/src/Ubictionary.LanguageServer.Tests/TestClient.fs @@ -1,7 +1,9 @@ namespace Ubictionary.LanguageServer.Tests +open System open System.IO.Pipelines open OmniSharp.Extensions.LanguageProtocol.Testing +open OmniSharp.Extensions.LanguageServer.Client open OmniSharp.Extensions.JsonRpc.Testing open Ubictionary.LanguageServer.Server @@ -16,5 +18,7 @@ type TestClient() = |> Async.Start (clientPipe.Reader.AsStream(), serverPipe.Writer.AsStream()) - member _.Initialize() = - base.InitializeClient(null) \ No newline at end of file + member _.Initialize clientOptsBuilder = + match clientOptsBuilder with + | Some f -> base.InitializeClient(Action(f)) + | None -> base.InitializeClient(null) \ No newline at end of file diff --git a/src/Ubictionary.LanguageServer.Tests/Ubictionary.LanguageServer.Tests.fsproj b/src/Ubictionary.LanguageServer.Tests/Ubictionary.LanguageServer.Tests.fsproj index 75202dd1..155f86bb 100644 --- a/src/Ubictionary.LanguageServer.Tests/Ubictionary.LanguageServer.Tests.fsproj +++ b/src/Ubictionary.LanguageServer.Tests/Ubictionary.LanguageServer.Tests.fsproj @@ -6,6 +6,8 @@ false + + diff --git a/src/Ubictionary.LanguageServer/Program.fs b/src/Ubictionary.LanguageServer/Program.fs index 17dd857b..4094c785 100644 --- a/src/Ubictionary.LanguageServer/Program.fs +++ b/src/Ubictionary.LanguageServer/Program.fs @@ -8,7 +8,7 @@ let setupLogging = Log.Logger <- LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() - .WriteTo.File("log.txt") + .WriteTo.File("log.txt", rollingInterval = RollingInterval.Day) .CreateLogger(); let private startWithConsole = diff --git a/src/Ubictionary.LanguageServer/Server.fs b/src/Ubictionary.LanguageServer/Server.fs index f886049e..f1cfc295 100644 --- a/src/Ubictionary.LanguageServer/Server.fs +++ b/src/Ubictionary.LanguageServer/Server.fs @@ -4,35 +4,38 @@ open System.Threading.Tasks open OmniSharp.Extensions.LanguageServer.Server open OmniSharp.Extensions.LanguageServer.Protocol.Server open OmniSharp.Extensions.LanguageServer.Protocol.Models +open OmniSharp.Extensions.LanguageServer.Protocol.Window +open Microsoft.Extensions.Configuration +open Microsoft.Extensions.Logging +open Microsoft.Extensions.DependencyInjection open Serilog open System.IO -open Microsoft.Extensions.DependencyInjection -open Microsoft.Extensions.Logging -let private onInitializeEvent msg server request response _cancellationToken = - Log.Logger.Information $"Ubictionary: {msg} {server} {request} {response}" - Task.CompletedTask -let private onInitialize s r = onInitializeEvent "Initializing" s r None -let private onInitialized = onInitializeEvent "Initialized" -let private onStarted s = onInitializeEvent "Started" s None None +let configSection = "ubictionary" + +let getConfig section key (config:IConfiguration) = + config.GetSection("ubictionary").Item("ubictionary_path") + +let private requestConfig (s:ILanguageServer) _cancellationToken = + async { + Log.Logger.Information "Getting config..." + let! config = + s.Configuration.GetConfiguration(ConfigurationItem(Section = configSection)) + |> Async.AwaitTask + let path = config |> getConfig configSection "ubictionary_path" + Log.Logger.Information $"Got path {path}" + s.Window.LogInfo $"Loading ubictionary from {path}" + } |> Async.StartAsTask :> Task let configureServer (input: Stream) (output: Stream) (opts:LanguageServerOptions) = opts .WithInput(input) .WithOutput(output) - .ConfigureLogging(fun b -> - b.AddLanguageProtocolLogging() - .AddSerilog(Log.Logger) - .SetMinimumLevel(LogLevel.Trace) - |> ignore) - .OnInitialize(OnLanguageServerInitializeDelegate(onInitialize)) - .OnInitialized(OnLanguageServerInitializedDelegate(onInitialized)) - .OnStarted(OnLanguageServerStartedDelegate(onStarted)) + .OnStarted(OnLanguageServerStartedDelegate(requestConfig)) + //.WithConfigurationSection(configSection) // Add back in when implementing didConfigurationChanged handling + .ConfigureLogging(fun z -> z.AddLanguageProtocolLogging().AddSerilog(Log.Logger).SetMinimumLevel(LogLevel.Trace) |> ignore) + // .WithServices(fun s -> s.AddLogging(fun b -> b.SetMinimumLevel(LogLevel.Trace) |> ignore) |> ignore) .WithServerInfo(ServerInfo(Name = "Ubictionary")) - .WithServices(fun x -> - x.AddLogging(fun b -> - b.SetMinimumLevel(LogLevel.Trace) |> ignore) - |> ignore) |> ignore