Skip to content

Commit f2bf9ab

Browse files
authored
Add diagnose command (#224)
* Add --diagnose command to perform basic diagnostics for lsp server interactively. * Add CHANGELOG.md entry * Add tests for --diagnose
1 parent 81e313f commit f2bf9ab

File tree

10 files changed

+214
-52
lines changed

10 files changed

+214
-52
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
## [Unreleased]
7+
* Add `--diagnose` command option to run basic diagnostics interactively
8+
- By @razzmatazz in https://github.com/razzmatazz/csharp-language-server/pull/224
79
* Fix `textDocument/codeAction` for extracting an interface
810
- By @alsi-lawr in https://github.com/razzmatazz/csharp-language-server/pull/267
911
* Will use static server capability registration, unless required (i.e. for workspace/didChangeWatchedFiles).

src/CSharpLanguageServer/CSharpLanguageServer.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<Compile Include="ProgressReporter.fs" />
3030
<Compile Include="RoslynHelpers.fs" />
3131
<Compile Include="DocumentationUtil.fs" />
32+
<Compile Include="Diagnostics.fs" />
3233
<Compile Include="Lsp/Client.fs" />
3334
<Compile Include="State/ServerState.fs" />
3435
<Compile Include="State/ServerRequestContext.fs" />
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
namespace CSharpLanguageServer
2+
3+
open System.IO
4+
open Microsoft.Extensions.Logging
5+
open Ionide.LanguageServerProtocol
6+
open Ionide.LanguageServerProtocol.Types
7+
open Ionide.LanguageServerProtocol.JsonRpc
8+
9+
open CSharpLanguageServer.Types
10+
open CSharpLanguageServer.Logging
11+
open CSharpLanguageServer.RoslynHelpers
12+
13+
module Diagnostics =
14+
let private logger = Logging.getLoggerByName "Diagnostics"
15+
16+
type LspClientStub() =
17+
interface System.IDisposable with
18+
member _.Dispose() = ()
19+
20+
interface ILspClient with
21+
member _.WindowShowMessage(p: ShowMessageParams) = async {
22+
logger.LogDebug("WindowShowMessage: {message}", p.Message)
23+
return ()
24+
}
25+
26+
member _.WindowLogMessage(p: LogMessageParams) = async {
27+
logger.LogDebug("WindowLogMessage: {message}", p.Message)
28+
return ()
29+
}
30+
31+
member _.TelemetryEvent(_: LSPAny) = async { return () }
32+
member _.TextDocumentPublishDiagnostics(_: PublishDiagnosticsParams) = async { return () }
33+
member _.LogTrace(_: LogTraceParams) = async { return () }
34+
member _.CancelRequest(_: CancelParams) = async { return () }
35+
member _.Progress(_: ProgressParams) = async { return () }
36+
member _.WorkspaceWorkspaceFolders() = async { return LspResult.Ok None }
37+
member _.WorkspaceConfiguration(_: ConfigurationParams) = async { return LspResult.Ok [||] }
38+
member _.WindowWorkDoneProgressCreate(_: WorkDoneProgressCreateParams) = async { return LspResult.Ok() }
39+
member _.WorkspaceSemanticTokensRefresh() = async { return LspResult.Ok() }
40+
member _.WindowShowDocument(_: ShowDocumentParams) = async { return LspResult.Ok { Success = false } }
41+
member _.WorkspaceInlineValueRefresh() = async { return LspResult.Ok() }
42+
member _.WorkspaceInlayHintRefresh() = async { return LspResult.Ok() }
43+
member _.WorkspaceDiagnosticRefresh() = async { return LspResult.Ok() }
44+
member _.ClientRegisterCapability(_: RegistrationParams) = async { return LspResult.Ok() }
45+
member _.ClientUnregisterCapability(_: UnregistrationParams) = async { return LspResult.Ok() }
46+
member _.WindowShowMessageRequest(_: ShowMessageRequestParams) = async { return LspResult.Ok None }
47+
member _.WorkspaceCodeLensRefresh() = async { return LspResult.Ok() }
48+
49+
member _.WorkspaceApplyEdit(_: ApplyWorkspaceEditParams) = async {
50+
return
51+
LspResult.Ok
52+
{ Applied = false
53+
FailureReason = None
54+
FailedChange = None }
55+
}
56+
57+
58+
let diagnose (settings: ServerSettings) : Async<int> = async {
59+
logger.LogDebug("diagnose: settings={settings}", settings)
60+
61+
initializeMSBuild logger
62+
63+
logger.LogDebug("diagnose: loading solution..")
64+
65+
let lspClient = new LspClientStub()
66+
let cwd = string (Directory.GetCurrentDirectory())
67+
let! _sln = loadSolutionOnSolutionPathOrDir lspClient logger None cwd
68+
69+
logger.LogDebug("diagnose: done")
70+
71+
let exitCode = 0
72+
return exitCode
73+
}

src/CSharpLanguageServer/Handlers/Initialization.fs

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ open System
44
open System.IO
55
open System.Reflection
66

7-
open Microsoft.Build.Locator
87
open Ionide.LanguageServerProtocol
98
open Ionide.LanguageServerProtocol.Types
109
open Ionide.LanguageServerProtocol.Server
@@ -15,6 +14,7 @@ open CSharpLanguageServer.State
1514
open CSharpLanguageServer.State.ServerState
1615
open CSharpLanguageServer.Types
1716
open CSharpLanguageServer.Logging
17+
open CSharpLanguageServer.RoslynHelpers
1818

1919
[<RequireQualifiedAccess>]
2020
module Initialization =
@@ -53,36 +53,7 @@ module Initialization =
5353
serverName
5454
)
5555

56-
let vsInstanceQueryOpt = VisualStudioInstanceQueryOptions.Default
57-
let vsInstanceList = MSBuildLocator.QueryVisualStudioInstances(vsInstanceQueryOpt)
58-
59-
if Seq.isEmpty vsInstanceList then
60-
raise (
61-
InvalidOperationException(
62-
"No instances of MSBuild could be detected."
63-
+ Environment.NewLine
64-
+ "Try calling RegisterInstance or RegisterMSBuildPath to manually register one."
65-
)
66-
)
67-
68-
// do! infoMessage "MSBuildLocator instances found:"
69-
//
70-
// for vsInstance in vsInstanceList do
71-
// do! infoMessage (sprintf "- SDK=\"%s\", Version=%s, MSBuildPath=\"%s\", DiscoveryType=%s"
72-
// vsInstance.Name
73-
// (string vsInstance.Version)
74-
// vsInstance.MSBuildPath
75-
// (string vsInstance.DiscoveryType))
76-
77-
let vsInstance = vsInstanceList |> Seq.head
78-
79-
logger.LogInformation(
80-
"MSBuildLocator: will register \"{vsInstanceName}\", Version={vsInstanceVersion} as default instance",
81-
vsInstance.Name,
82-
(string vsInstance.Version)
83-
)
84-
85-
MSBuildLocator.RegisterInstance(vsInstance)
56+
initializeMSBuild logger
8657

8758
logger.LogDebug("handleInitialize: p.Capabilities={caps}", serialize p.Capabilities)
8859
context.Emit(ClientCapabilityChange p.Capabilities)

src/CSharpLanguageServer/Program.fs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module CSharpLanguageServer.Program
22

3+
open System
34
open System.Reflection
45

56
open Argu
@@ -8,20 +9,34 @@ open Microsoft.Extensions.Logging
89
open CSharpLanguageServer.Types
910
open CSharpLanguageServer.Lsp
1011
open CSharpLanguageServer.Logging
12+
open CSharpLanguageServer.Diagnostics
13+
1114

1215
type CLIArguments =
1316
| [<AltCommandLine("-v")>] Version
1417
| [<AltCommandLine("-l")>] LogLevel of level: string
1518
| [<AltCommandLine("-s")>] Solution of solution: string
1619
| Debug
20+
| Diagnose
1721

1822
interface IArgParserTemplate with
1923
member s.Usage =
2024
match s with
21-
| Version -> "display versioning information"
22-
| Solution _ -> ".sln file to load (relative to CWD)"
23-
| LogLevel _ -> "log level, <trace|debug|info|warning|error>; default is `info`"
24-
| Debug -> "enable debug mode"
25+
| Version -> "Display versioning information"
26+
| Solution _ -> "Specify .sln file to load (relative to CWD)"
27+
| LogLevel _ -> "Set log level, <trace|debug|info|warning|error>; default is `info`"
28+
| Debug -> "Enable debug mode"
29+
| Diagnose -> "Run diagnostics"
30+
31+
32+
let parseLogLevel (debugMode: bool) (logLevelArg: string option) =
33+
match logLevelArg with
34+
| Some "error" -> LogLevel.Error
35+
| Some "warning" -> LogLevel.Warning
36+
| Some "info" -> LogLevel.Information
37+
| Some "debug" -> LogLevel.Debug
38+
| Some "trace" -> LogLevel.Trace
39+
| _ -> if debugMode then LogLevel.Debug else LogLevel.Information
2540

2641

2742
[<EntryPoint>]
@@ -39,27 +54,24 @@ let entry args =
3954
exit 0)
4055

4156
let debugMode: bool = serverArgs.Contains Debug
42-
43-
let logLevelArg = serverArgs.TryGetResult <@ LogLevel @>
44-
45-
let logLevel =
46-
match logLevelArg with
47-
| Some "error" -> LogLevel.Error
48-
| Some "warning" -> LogLevel.Warning
49-
| Some "info" -> LogLevel.Information
50-
| Some "debug" -> LogLevel.Debug
51-
| Some "trace" -> LogLevel.Trace
52-
| _ -> if debugMode then LogLevel.Debug else LogLevel.Information
57+
let logLevel = serverArgs.TryGetResult(<@ LogLevel @>) |> parseLogLevel debugMode
5358

5459
let settings =
5560
{ ServerSettings.Default with
61+
DebugMode = debugMode
5662
SolutionPath = serverArgs.TryGetResult <@ Solution @>
57-
LogLevel = logLevel
58-
DebugMode = debugMode }
59-
60-
Logging.setupLogging settings.LogLevel
61-
62-
Server.start settings
63+
LogLevel = logLevel }
64+
65+
let exitCode =
66+
match serverArgs.TryGetResult <@ Diagnose @> with
67+
| Some _ ->
68+
Logging.setupLogging LogLevel.Trace
69+
diagnose settings |> Async.RunSynchronously
70+
| _ ->
71+
Logging.setupLogging settings.LogLevel
72+
Server.start settings
73+
74+
exitCode
6375
with
6476
| :? ArguParseException as ex ->
6577
eprintfn "%s" ex.Message

src/CSharpLanguageServer/RoslynHelpers.fs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ open System.Threading
88
open System.Threading.Tasks
99
open System.Collections.Immutable
1010
open System.Text.RegularExpressions
11+
12+
open Microsoft.Build.Locator
1113
open Castle.DynamicProxy
1214
open ICSharpCode.Decompiler
1315
open ICSharpCode.Decompiler.CSharp
@@ -834,3 +836,39 @@ let makeDocumentFromMetadata
834836

835837
let mdDocument = SourceText.From text |> mdDocumentEmpty.WithText
836838
mdDocument, text
839+
840+
841+
let initializeMSBuild (logger: ILogger) : unit =
842+
let vsInstanceQueryOpt = VisualStudioInstanceQueryOptions.Default
843+
let vsInstanceList = MSBuildLocator.QueryVisualStudioInstances(vsInstanceQueryOpt)
844+
845+
if Seq.isEmpty vsInstanceList then
846+
raise (
847+
InvalidOperationException(
848+
"No instances of MSBuild could be detected."
849+
+ Environment.NewLine
850+
+ "Try calling RegisterInstance or RegisterMSBuildPath to manually register one."
851+
)
852+
)
853+
854+
logger.LogTrace("MSBuildLocator instances found:")
855+
856+
for vsInstance in vsInstanceList do
857+
logger.LogTrace(
858+
sprintf
859+
"- SDK=\"%s\", Version=%s, MSBuildPath=\"%s\", DiscoveryType=%s"
860+
vsInstance.Name
861+
(string vsInstance.Version)
862+
vsInstance.MSBuildPath
863+
(string vsInstance.DiscoveryType)
864+
)
865+
866+
let vsInstance = vsInstanceList |> Seq.head
867+
868+
logger.LogInformation(
869+
"MSBuildLocator: will register \"{vsInstanceName}\", Version={vsInstanceVersion} as default instance",
870+
vsInstance.Name,
871+
(string vsInstance.Version)
872+
)
873+
874+
MSBuildLocator.RegisterInstance(vsInstance)

tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<Compile Include="CompletionTests.fs" />
2323
<Compile Include="WorkspaceSymbolTests.fs" />
2424
<Compile Include="SemanticTokenTests.fs" />
25+
<Compile Include="DiagnoseCommandTests.fs" />
2526
</ItemGroup>
2627

2728
<ItemGroup>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module CSharpLanguageServer.Tests.DiagnoseCommandTests
2+
3+
open System.IO
4+
open System.Reflection
5+
open System.Diagnostics
6+
open System.Threading.Tasks
7+
8+
open NUnit.Framework
9+
10+
open CSharpLanguageServer.Tests.Tooling
11+
12+
13+
[<TestCase>]
14+
let testDiagnoseCommandWorks () =
15+
let testDataDirName = "testDiagnoseCommandWorks"
16+
17+
let testAssemblyLocationDir =
18+
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
19+
20+
let actualTestDataDir =
21+
DirectoryInfo(Path.Combine(testAssemblyLocationDir, "..", "..", "..", "TestData", testDataDirName))
22+
|> _.FullName
23+
24+
let processStartInfo = makeServerProcessInfo actualTestDataDir
25+
processStartInfo.Arguments <- "--diagnose"
26+
27+
let p = new Process()
28+
p.StartInfo <- processStartInfo
29+
30+
let startResult = p.Start()
31+
32+
if not startResult then
33+
failwith "Failed to start server process."
34+
35+
let stdoutTask = p.StandardOutput.ReadToEndAsync()
36+
let stderrTask = p.StandardError.ReadToEndAsync()
37+
38+
p.WaitForExit(1000 * 10) |> ignore
39+
40+
if not p.HasExited then
41+
p.Kill()
42+
p.WaitForExit()
43+
44+
Task.WaitAll(stdoutTask, stderrTask)
45+
46+
let stdout: string = stdoutTask.Result
47+
Assert.IsEmpty(stdout)
48+
49+
let stderr: string = stderrTask.Result
50+
Assert.IsTrue(stderr.Contains("diagnose: loading solution.."))
51+
Assert.IsTrue(stderr.Contains("csharp-ls: loading project"))
52+
Assert.IsTrue(stderr.Contains("diagnose: done"))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class Class
2+
{
3+
public void MethodA(string arg)
4+
{
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net9.0</TargetFramework>
5+
</PropertyGroup>
6+
</Project>

0 commit comments

Comments
 (0)