├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── DotNetLanguageClient.sln ├── DotNetLanguageClient.snk ├── LICENSE ├── NuGet.config ├── README.md ├── Version.props ├── samples ├── Client │ ├── Client.csproj │ └── Program.cs ├── Common │ ├── Common.csproj │ ├── ConfigurationHandler.cs │ ├── Dummy.cs │ └── HoverHandler.cs ├── Samples.props ├── Server │ ├── Program.cs │ └── Server.csproj ├── SingleProcess │ ├── Program.cs │ └── SingleProcess.csproj └── VisualStudioExtension │ ├── AssemblyDependencies.cs │ ├── ExtensionPackage.cs │ ├── Key.snk │ ├── LspQuickInfoProvider.cs │ ├── LspQuickInfoSource.cs │ ├── Markdown.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── README.md │ ├── Resources │ └── ExtensionPackage.ico │ ├── TextViewCreationListener.cs │ ├── ThreadAffinitiveSynchronizationContext.cs │ ├── VSPackage.resx │ ├── VisualStudioApiExtensions.cs │ ├── VisualStudioExtension.csproj │ ├── app.config │ ├── packages.config │ └── source.extension.vsixmanifest ├── src ├── Common.props └── LSP.Client │ ├── Clients │ ├── TextDocumentClient.Completions.cs │ ├── TextDocumentClient.Diagnostics.cs │ ├── TextDocumentClient.Hover.cs │ ├── TextDocumentClient.Sync.cs │ ├── TextDocumentClient.cs │ ├── WindowClient.cs │ └── WorkspaceClient.cs │ ├── Dispatcher │ ├── LspDispatcher.cs │ └── LspDispatcherExtensions.cs │ ├── Exceptions.cs │ ├── HandlerDelegates.cs │ ├── Handlers │ ├── DelegateEmptyNotificationHandler.cs │ ├── DelegateHandler.cs │ ├── DelegateNotificationHandler.cs │ ├── DelegateRequestHandler.cs │ ├── DelegateRequestResponseHandler.cs │ ├── DynamicRegistrationHandler.cs │ ├── HandlerKind.cs │ ├── IHandler.cs │ ├── IInvokeEmptyNotificationHandler.cs │ ├── IInvokeNotificationHandler.cs │ ├── IInvokeRequestHandler.cs │ ├── JsonRpcEmptyNotificationHandler.cs │ ├── JsonRpcHandler.cs │ └── JsonRpcNotificationHandler.cs │ ├── LSP.Client.csproj │ ├── LanguageClient.cs │ ├── LanguageClientRegistration.cs │ ├── Logging │ ├── OverwriteSourceContextEnricher.cs │ └── SerilogExtensions.cs │ ├── LspErrorCodes.cs │ ├── Processes │ ├── NamedPipeServerProcess.cs │ ├── ServerProcess.cs │ └── StdioServerProcess.cs │ ├── Protocol │ ├── ClientMessage.cs │ ├── ErrorMessage.cs │ ├── LspConnection.cs │ ├── LspConnectionExtensions.cs │ └── ServerMessage.cs │ └── Utilities │ └── DocumentUri.cs └── test ├── LSP.Client.Tests ├── ConnectionTests.cs ├── LSP.Client.Tests.csproj ├── Logging │ ├── SerilogTestOutputExtensions.cs │ └── TestOutputSink.cs ├── PipeServerTestBase.cs ├── PipeTests.cs └── TestBase.cs └── TestCommon.props /.gitignore: -------------------------------------------------------------------------------- 1 | ## VS Code 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | 8 | # Mac 9 | .DS_Store 10 | 11 | ## Visual Studio 12 | 13 | # User-specific files 14 | *.suo 15 | *.user 16 | *.userosscache 17 | *.sln.docstates 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | 34 | # Visual Studio 2015 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # MSTest test Results 40 | [Tt]est[Rr]esult*/ 41 | [Bb]uild[Ll]og.* 42 | 43 | # NUNIT 44 | *.VisualState.xml 45 | TestResult.xml 46 | 47 | # Build Results of an ATL Project 48 | [Dd]ebugPS/ 49 | [Rr]eleasePS/ 50 | dlldata.c 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | **/Properties/launchSettings.json 57 | 58 | *_i.c 59 | *_p.c 60 | *_i.h 61 | *.ilk 62 | *.meta 63 | *.obj 64 | *.pch 65 | *.pdb 66 | *.pgc 67 | *.pgd 68 | *.rsp 69 | *.sbr 70 | *.tlb 71 | *.tli 72 | *.tlh 73 | *.tmp 74 | *.tmp_proj 75 | *.log 76 | *.vspscc 77 | *.vssscc 78 | .builds 79 | *.pidb 80 | *.svclog 81 | *.scc 82 | 83 | # Chutzpah Test files 84 | _Chutzpah* 85 | 86 | # Visual C++ cache files 87 | ipch/ 88 | *.aps 89 | *.ncb 90 | *.opendb 91 | *.opensdf 92 | *.sdf 93 | *.cachefile 94 | *.VC.db 95 | *.VC.VC.opendb 96 | 97 | # Visual Studio profiler 98 | *.psess 99 | *.vsp 100 | *.vspx 101 | *.sap 102 | 103 | # TFS 2012 Local Workspace 104 | $tf/ 105 | 106 | # Guidance Automation Toolkit 107 | *.gpState 108 | 109 | # ReSharper is a .NET coding add-in 110 | _ReSharper*/ 111 | *.[Rr]e[Ss]harper 112 | *.DotSettings.user 113 | 114 | # JustCode is a .NET coding add-in 115 | .JustCode 116 | 117 | # TeamCity is a build add-in 118 | _TeamCity* 119 | 120 | # DotCover is a Code Coverage Tool 121 | *.dotCover 122 | 123 | # Visual Studio code coverage results 124 | *.coverage 125 | *.coveragexml 126 | 127 | # NCrunch 128 | _NCrunch_* 129 | .*crunch*.local.xml 130 | nCrunchTemp_* 131 | 132 | # MightyMoose 133 | *.mm.* 134 | AutoTest.Net/ 135 | 136 | # Web workbench (sass) 137 | .sass-cache/ 138 | 139 | # Installshield output folder 140 | [Ee]xpress/ 141 | 142 | # DocProject is a documentation generator add-in 143 | DocProject/buildhelp/ 144 | DocProject/Help/*.HxT 145 | DocProject/Help/*.HxC 146 | DocProject/Help/*.hhc 147 | DocProject/Help/*.hhk 148 | DocProject/Help/*.hhp 149 | DocProject/Help/Html2 150 | DocProject/Help/html 151 | 152 | # Click-Once directory 153 | publish/ 154 | 155 | # Publish Web Output 156 | *.[Pp]ublish.xml 157 | *.azurePubxml 158 | # TODO: Comment the next line if you want to checkin your web deploy settings 159 | # but database connection strings (with potential passwords) will be unencrypted 160 | *.pubxml 161 | *.publishproj 162 | 163 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 164 | # checkin your Azure Web App publish settings, but sensitive information contained 165 | # in these scripts will be unencrypted 166 | PublishScripts/ 167 | 168 | # NuGet Packages 169 | **/packages/* 170 | # except build/, which is used as an MSBuild target. 171 | !**/packages/build/ 172 | # Uncomment if necessary however generally it will be regenerated when needed 173 | #!**/packages/repositories.config 174 | # NuGet v3's project.json files produces more ignorable files 175 | *.nuget.props 176 | *.nuget.targets 177 | 178 | # Microsoft Azure Build Output 179 | csx/ 180 | *.build.csdef 181 | 182 | # Microsoft Azure Emulator 183 | ecf/ 184 | rcf/ 185 | 186 | # Windows Store app package directories and files 187 | AppPackages/ 188 | BundleArtifacts/ 189 | Package.StoreAssociation.xml 190 | _pkginfo.txt 191 | 192 | # Visual Studio cache files 193 | # files ending in .cache can be ignored 194 | *.[Cc]ache 195 | # but keep track of directories ending in .cache 196 | !*.[Cc]ache/ 197 | 198 | # Others 199 | ClientBin/ 200 | ~$* 201 | *~ 202 | *.dbmdl 203 | *.dbproj.schemaview 204 | *.jfm 205 | *.pfx 206 | *.publishsettings 207 | orleans.codegen.cs 208 | 209 | # Since there are multiple workflows, uncomment next line to ignore bower_components 210 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 211 | #bower_components/ 212 | 213 | # RIA/Silverlight projects 214 | Generated_Code/ 215 | 216 | # Backup & report files from converting an old project file 217 | # to a newer Visual Studio version. Backup files are not needed, 218 | # because we have git ;-) 219 | _UpgradeReport_Files/ 220 | Backup*/ 221 | UpgradeLog*.XML 222 | UpgradeLog*.htm 223 | 224 | # SQL Server files 225 | *.mdf 226 | *.ldf 227 | *.ndf 228 | 229 | # Business Intelligence projects 230 | *.rdl.data 231 | *.bim.layout 232 | *.bim_*.settings 233 | 234 | # Microsoft Fakes 235 | FakesAssemblies/ 236 | 237 | # GhostDoc plugin setting file 238 | *.GhostDoc.xml 239 | 240 | # Node.js Tools for Visual Studio 241 | .ntvs_analysis.dat 242 | node_modules/ 243 | 244 | # Typescript v1 declaration files 245 | typings/ 246 | 247 | # Visual Studio 6 build log 248 | *.plg 249 | 250 | # Visual Studio 6 workspace options file 251 | *.opt 252 | 253 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 254 | *.vbw 255 | 256 | # Visual Studio LightSwitch build output 257 | **/*.HTMLClient/GeneratedArtifacts 258 | **/*.DesktopClient/GeneratedArtifacts 259 | **/*.DesktopClient/ModelManifest.xml 260 | **/*.Server/GeneratedArtifacts 261 | **/*.Server/ModelManifest.xml 262 | _Pvt_Extensions 263 | 264 | # Paket dependency manager 265 | .paket/paket.exe 266 | paket-files/ 267 | 268 | # FAKE - F# Make 269 | .fake/ 270 | 271 | # JetBrains Rider 272 | .idea/ 273 | *.sln.iml 274 | 275 | # CodeRush 276 | .cr/ 277 | 278 | # Python Tools for Visual Studio (PTVS) 279 | __pycache__/ 280 | *.pyc 281 | 282 | # Cake - Uncomment if you are using it 283 | # tools/** 284 | # !tools/packages.config 285 | 286 | # Telerik's JustMock configuration file 287 | *.jmconfig 288 | 289 | # BizTalk build output 290 | *.btp.cs 291 | *.btm.cs 292 | *.odx.cs 293 | *.xsd.cs 294 | 295 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceRoot}/samples/Client/bin/Debug/netcoreapp2.0/Client.dll", 14 | "args": [], 15 | "cwd": "${workspaceRoot}/samples/Client", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "dotnet", 4 | "isShellCommand": true, 5 | "args": [], 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "args": [ 10 | "${workspaceRoot}/samples/Client/Client.csproj" 11 | ], 12 | "isBuildCommand": true, 13 | "problemMatcher": "$msCompile" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /DotNetLanguageClient.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tintoy/dotnet-language-client/208f65cf7e5080ed345fd7be19d2d4764d3393ba/DotNetLanguageClient.snk -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-language-client 2 | .NET client for the Language Server Protocol (LSP) 3 | 4 | **NOTE:** this code has been merged into OmniSharp's [csharp-language-server-protocol](https://github.com/OmniSharp/csharp-language-server-protocol/tree/master/src/Client) (NuGet package [here](https://www.nuget.org/packages/OmniSharp.Extensions.LanguageClient/)). 5 | 6 | ## Usage 7 | 8 | ```csharp 9 | ProcessStartInfo serverStartInfo = new ProcessStartInfo("dotnet") 10 | { 11 | Arguments = $"\"{ServerAssembly}\" arg1 arg2 arg3", 12 | Environment = 13 | { 14 | ["SomeVar"] = "Foo" 15 | } 16 | }; 17 | 18 | Log.Information("Starting server..."); 19 | using (LanguageClient client = new LanguageClient(serverStartInfo)) 20 | { 21 | client.HandleNotification("dummy/notify", () => 22 | { 23 | Log.Information("Received dummy notification from language server."); 24 | }); 25 | 26 | await client.Start(); 27 | 28 | Log.Information("Client started."); 29 | 30 | Log.Information("Sending 'initialize' request..."); 31 | InitializeResult initializeResult = await client.SendRequest("initialize", new InitializeParams 32 | { 33 | RootPath = @"C:\Foo", 34 | Capabilities = new ClientCapabilities 35 | { 36 | Workspace = new WorkspaceClientCapabilites 37 | { 38 | 39 | }, 40 | TextDocument = new TextDocumentClientCapabilities 41 | { 42 | 43 | } 44 | } 45 | }); 46 | Log.Information("Received InitializeResult {@InitializeResult}...", initializeResult); 47 | 48 | Log.Information("Sending 'dummy' request..."); 49 | await client.SendRequest("dummy", new DummyParams 50 | { 51 | Message = "Hello, world!" 52 | }); 53 | 54 | Log.Information("Stopping language server..."); 55 | await client.Stop(); 56 | Log.Information("Server stopped."); 57 | } 58 | ``` 59 | 60 | ## Visual Studio Extension Sample 61 | 62 | > What's with all the assemblies in the project folder? 63 | 64 | VS won't find our assembly dependencies (a bigger issue for .NET Standard assemblies) unless we include them in the VSIX and provide a custom code-base (see `AssemblyDependencies.cs`). 65 | There's a custom target in the project to include some of the, but I haven't had time to include the others (e.g. `Serilog` and friends). 66 | 67 | I haven't had time to reorganise the assembly dependency stuff yet, but there's probably a much cleaner way to do it. 68 | -------------------------------------------------------------------------------- /Version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.0 4 | 5 | <_VersionSuffix>$(VersionSuffix) 6 | <_VersionSuffix Condition=" '$(VersionSuffix)' == '' ">dev 7 | <_VersionSuffix Condition=" '$(VersionSuffix)' == 'release' "> 8 | $(_VersionSuffix) 9 | 10 | -------------------------------------------------------------------------------- /samples/Client/Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /samples/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using OmniSharp.Extensions.LanguageServer.Capabilities.Client; 3 | using LSP.Client; 4 | using Newtonsoft.Json.Linq; 5 | using Serilog; 6 | using Serilog.Events; 7 | using System; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace Client 14 | { 15 | /// 16 | /// A simple demo of using the to interact with a language server. 17 | /// 18 | static class Program 19 | { 20 | /// 21 | /// The full path to the assembly that implements the language server. 22 | /// 23 | static readonly string ServerAssembly = Path.GetFullPath(Path.Combine( 24 | Path.GetDirectoryName(typeof(Program).Assembly.Location), "..", "..", "..", "..", 25 | "Server/bin/Debug/netcoreapp2.0/Server.dll".Replace('/', Path.DirectorySeparatorChar) 26 | )); 27 | 28 | /// 29 | /// The main program entry-point. 30 | /// 31 | static void Main() 32 | { 33 | SynchronizationContext.SetSynchronizationContext( 34 | new SynchronizationContext() 35 | ); 36 | 37 | ConfigureLogging(); 38 | 39 | try 40 | { 41 | AsyncMain().Wait(); 42 | } 43 | catch (AggregateException unexpectedError) 44 | { 45 | foreach (Exception exception in unexpectedError.Flatten().InnerExceptions) 46 | Log.Error(exception, "Unexpected error."); 47 | } 48 | catch (Exception unexpectedError) 49 | { 50 | Log.Error(unexpectedError, "Unexpected error."); 51 | } 52 | finally 53 | { 54 | Log.CloseAndFlush(); 55 | } 56 | } 57 | 58 | /// 59 | /// The main asynchronous program entry-point. 60 | /// 61 | /// 62 | /// A representing program operation. 63 | /// 64 | static async Task AsyncMain() 65 | { 66 | ProcessStartInfo serverStartInfo = new ProcessStartInfo("dotnet") 67 | { 68 | Arguments = $"\"{ServerAssembly}\"" 69 | }; 70 | 71 | Log.Information("Starting server..."); 72 | LanguageClient client = new LanguageClient(Log.Logger, serverStartInfo) 73 | { 74 | ClientCapabilities = 75 | { 76 | Workspace = 77 | { 78 | DidChangeConfiguration = new DidChangeConfigurationCapability 79 | { 80 | DynamicRegistration = false 81 | } 82 | } 83 | } 84 | }; 85 | using (client) 86 | { 87 | // Listen for log messages from the language server. 88 | client.Window.OnLogMessage((message, messageType) => 89 | { 90 | Log.Information("Language server says: [{MessageType:l}] {Message}", messageType, message); 91 | }); 92 | 93 | // Listen for our custom notification from the language server. 94 | client.HandleNotification("dummy/notify", notification => 95 | { 96 | Log.Information("Received dummy notification from language server: {Message}", 97 | notification.Message 98 | ); 99 | }); 100 | 101 | await client.Initialize(workspaceRoot: @"C:\Foo"); 102 | 103 | Log.Information("Client started."); 104 | 105 | // Update server configuration. 106 | client.Workspace.DidChangeConfiguration( 107 | new JObject( 108 | new JProperty("setting1", true), 109 | new JProperty("setting2", "Hello") 110 | ) 111 | ); 112 | 113 | // Invoke our custom handler. 114 | await client.SendRequest("dummy", new DummyParams 115 | { 116 | Message = "Hello, world!" 117 | }); 118 | 119 | Log.Information("Stopping language server..."); 120 | await client.Shutdown(); 121 | Log.Information("Server stopped."); 122 | } 123 | } 124 | 125 | /// 126 | /// Configure the global logger. 127 | /// 128 | static void ConfigureLogging() 129 | { 130 | LogEventLevel logLevel = 131 | Environment.GetEnvironmentVariable("LSP_VERBOSE_LOGGING") == "1" 132 | ? LogEventLevel.Verbose 133 | : LogEventLevel.Information; 134 | 135 | LoggerConfiguration loggerConfiguration = 136 | new LoggerConfiguration() 137 | .MinimumLevel.Verbose() 138 | .Enrich.WithProperty("ProcessId", Process.GetCurrentProcess().Id) 139 | .Enrich.WithProperty("Source", "Client") 140 | .WriteTo.Debug( 141 | restrictedToMinimumLevel: logLevel 142 | ); 143 | 144 | string seqUrl = Environment.GetEnvironmentVariable("LSP_SEQ_URL"); 145 | if (!String.IsNullOrWhiteSpace(seqUrl)) 146 | { 147 | loggerConfiguration = loggerConfiguration.WriteTo.Seq(seqUrl, 148 | apiKey: Environment.GetEnvironmentVariable("LSP_SEQ_API_KEY"), 149 | restrictedToMinimumLevel: logLevel 150 | ); 151 | } 152 | 153 | string logFile = Environment.GetEnvironmentVariable("LSP_LOG_FILE"); 154 | if (!String.IsNullOrWhiteSpace(logFile)) 155 | { 156 | string logExtension = Path.GetExtension(logFile); 157 | logFile = Path.GetFullPath( 158 | Path.ChangeExtension(logFile, ".Client" + logExtension) 159 | ); 160 | 161 | loggerConfiguration = loggerConfiguration.WriteTo.File(logFile, 162 | restrictedToMinimumLevel: logLevel 163 | ); 164 | } 165 | 166 | Log.Logger = loggerConfiguration.CreateLogger(); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /samples/Common/Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/Common/ConfigurationHandler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Abstractions; 2 | using OmniSharp.Extensions.LanguageServer.Capabilities.Client; 3 | using OmniSharp.Extensions.LanguageServer.Models; 4 | using OmniSharp.Extensions.LanguageServer.Protocol; 5 | using Serilog; 6 | using System.Threading.Tasks; 7 | 8 | namespace Common 9 | { 10 | /// 11 | /// Handler for "workspace/didChangeConfiguration" notifications. 12 | /// 13 | public class ConfigurationHandler 14 | : IDidChangeConfigurationHandler 15 | { 16 | /// 17 | /// The client-side capabilities for DidChangeConfiguration. 18 | /// 19 | public DidChangeConfigurationCapability Capabilities { get; private set; } 20 | 21 | /// 22 | /// Handle a "workspace/didChangeConfiguration" notification. 23 | /// 24 | /// 25 | /// The notification message. 26 | /// 27 | /// 28 | /// A representing the operation. 29 | /// 30 | public Task Handle(DidChangeConfigurationParams notification) 31 | { 32 | Log.Information("Received DidChangeConfiguration notification: {@Settings}", notification.Settings); 33 | 34 | return Task.CompletedTask; 35 | } 36 | 37 | /// 38 | /// Called to notify the handler of the client-side capabilities for DidChangeconfiguration. 39 | /// 40 | /// 41 | /// A representing the capabilities. 42 | /// 43 | void ICapability.SetCapability(DidChangeConfigurationCapability capabilities) 44 | { 45 | Log.Information("ConfigurationHandler recieved capability: {@Capability}", capabilities); 46 | 47 | Capabilities = capabilities; 48 | } 49 | 50 | /// 51 | /// Get registration options (unused). 52 | /// 53 | /// 54 | /// null 55 | /// 56 | object IRegistration.GetRegistrationOptions() => null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /samples/Common/Dummy.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using OmniSharp.Extensions.LanguageServer; 3 | using OmniSharp.Extensions.LanguageServer.Models; 4 | using OmniSharp.Extensions.LanguageServer.Protocol; 5 | using Newtonsoft.Json; 6 | using Serilog; 7 | using System; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Common 12 | { 13 | /// 14 | /// Parameters for the "dummy" request and "dummy/notify" notification. 15 | /// 16 | public class DummyParams 17 | { 18 | /// 19 | /// A textual message (der). 20 | /// 21 | [JsonProperty("message")] 22 | public string Message { get; set; } 23 | } 24 | 25 | /// 26 | /// Represents a handler for the "dummy" request. 27 | /// 28 | [Method("dummy")] 29 | public interface IDummyRequestHandler 30 | : IRequestHandler 31 | { 32 | } 33 | 34 | /// 35 | /// A handler for the "dummy" request. 36 | /// 37 | public class DummyHandler 38 | : IDummyRequestHandler 39 | { 40 | /// 41 | /// Create a new . 42 | /// 43 | /// 44 | /// The language server. 45 | /// 46 | public DummyHandler(ILanguageServer server) 47 | { 48 | if (server == null) 49 | throw new ArgumentNullException(nameof(server)); 50 | 51 | Server = server; 52 | } 53 | 54 | /// 55 | /// The language server. 56 | /// 57 | ILanguageServer Server { get; } 58 | 59 | /// 60 | /// Handle the "dummy" request. 61 | /// 62 | /// 63 | /// The request parameters. 64 | /// 65 | /// 66 | /// A that can be used to cancel the request. 67 | /// 68 | /// 69 | /// A representing the operation. 70 | /// 71 | public Task Handle(DummyParams request, CancellationToken cancellationToken) 72 | { 73 | Log.Information("DummyHandler got request {@Request}", request); 74 | 75 | Server.LogMessage(new LogMessageParams 76 | { 77 | Message = "Hello from DummyHandler :-)", 78 | Type = MessageType.Info 79 | }); 80 | 81 | char[] message = request.Message.ToCharArray(); 82 | Array.Reverse(message); 83 | 84 | Server.SendNotification("dummy/notify", new DummyParams 85 | { 86 | Message = new String(message) 87 | }); 88 | 89 | return Task.CompletedTask; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /samples/Common/HoverHandler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Protocol; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using OmniSharp.Extensions.LanguageServer.Capabilities.Client; 6 | using OmniSharp.Extensions.LanguageServer.Models; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Common 11 | { 12 | /// 13 | /// Handler for LSP "textDocument/hover" requests. 14 | /// 15 | public class HoverHandler 16 | : IHoverHandler 17 | { 18 | /// 19 | /// Create a new . 20 | /// 21 | public HoverHandler() 22 | { 23 | } 24 | 25 | /// 26 | /// Registration options for the hover handler. 27 | /// 28 | public TextDocumentRegistrationOptions TextDocumentRegistrationOptions { get; } = new TextDocumentRegistrationOptions 29 | { 30 | DocumentSelector = new DocumentSelector( 31 | new DocumentFilter 32 | { 33 | Language = "xml", 34 | Pattern = "**/*.csproj" 35 | } 36 | ) 37 | }; 38 | 39 | /// 40 | /// The client's hover capabilities. 41 | /// 42 | public HoverCapability Capabilities { get; private set; } 43 | 44 | /// 45 | /// Handle a hover request. 46 | /// 47 | /// 48 | /// The hover request. 49 | /// 50 | /// 51 | /// A cancellation token that can be used to cancel the request. 52 | /// 53 | /// 54 | /// The , or null if no hover information is available at the target document position. 55 | /// 56 | public Task Handle(TextDocumentPositionParams request, CancellationToken token) 57 | { 58 | return Task.FromResult(new Hover 59 | { 60 | Range = new Range( 61 | start: request.Position, 62 | end: request.Position 63 | ), 64 | Contents = new MarkedStringContainer( 65 | $"Hover for {request.Position.Line + 1},{request.Position.Character + 1} in '{request.TextDocument.Uri}'." 66 | ) 67 | }); 68 | } 69 | 70 | /// 71 | /// Get registration options for the hover handler. 72 | /// 73 | /// 74 | /// The . 75 | /// 76 | public TextDocumentRegistrationOptions GetRegistrationOptions() => TextDocumentRegistrationOptions; 77 | 78 | /// 79 | /// Called to provide information about the client's hover capabilities. 80 | /// 81 | /// 82 | /// The client's hover capabilities. 83 | /// 84 | public void SetCapability(HoverCapability capabilities) 85 | { 86 | Capabilities = capabilities; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /samples/Samples.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | IDE0016 4 | false 5 | true 6 | 7 | -------------------------------------------------------------------------------- /samples/Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using OmniSharp.Extensions.LanguageServer; 3 | using Serilog; 4 | using Serilog.Events; 5 | using System; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | using MSLogging = Microsoft.Extensions.Logging; 12 | 13 | namespace Server 14 | { 15 | /// 16 | /// A simple language server called by the language client. 17 | /// 18 | static class Program 19 | { 20 | /// 21 | /// The main program entry-point. 22 | /// 23 | static void Main() 24 | { 25 | SynchronizationContext.SetSynchronizationContext( 26 | new SynchronizationContext() 27 | ); 28 | 29 | ConfigureLogging(); 30 | 31 | try 32 | { 33 | AsyncMain().Wait(); 34 | 35 | Log.Information("All done, terminating..."); 36 | } 37 | catch (AggregateException unexpectedError) 38 | { 39 | foreach (Exception exception in unexpectedError.Flatten().InnerExceptions) 40 | Log.Error(exception, "Unexpected error."); 41 | } 42 | catch (Exception unexpectedError) 43 | { 44 | Log.Error(unexpectedError, "Unexpected error."); 45 | } 46 | finally 47 | { 48 | Log.CloseAndFlush(); 49 | } 50 | } 51 | 52 | /// 53 | /// The main asynchronous program entry-point. 54 | /// 55 | /// 56 | /// A representing program operation. 57 | /// 58 | static async Task AsyncMain() 59 | { 60 | Log.Information("Initialising language server..."); 61 | 62 | LanguageServer languageServer = new LanguageServer( 63 | input: Console.OpenStandardInput(2048), 64 | output: Console.OpenStandardOutput(2048), 65 | loggerFactory: new MSLogging.LoggerFactory().AddSerilog(Log.Logger.ForContext()) 66 | ); 67 | languageServer.AddHandler( 68 | new ConfigurationHandler() 69 | ); 70 | languageServer.AddHandler( 71 | new DummyHandler(languageServer) 72 | ); 73 | 74 | Log.Information("Starting language server..."); 75 | var initTask = languageServer.Initialize(); 76 | 77 | languageServer.Shutdown += shutdownRequested => 78 | { 79 | Log.Information("Language server shutdown (ShutDownRequested={ShutDownRequested}).", shutdownRequested); 80 | }; 81 | 82 | Log.Information("Language server initialised; waiting for shutdown."); 83 | 84 | await initTask; 85 | 86 | Log.Information("Waiting for shutdown..."); 87 | 88 | await languageServer.WasShutDown; 89 | } 90 | 91 | /// 92 | /// Configure the global logger. 93 | /// 94 | static void ConfigureLogging() 95 | { 96 | LogEventLevel logLevel = 97 | Environment.GetEnvironmentVariable("LSP_VERBOSE_LOGGING") == "1" 98 | ? LogEventLevel.Verbose 99 | : LogEventLevel.Information; 100 | 101 | LoggerConfiguration loggerConfiguration = 102 | new LoggerConfiguration() 103 | .MinimumLevel.Verbose() 104 | .Enrich.WithProperty("ProcessId", Process.GetCurrentProcess().Id) 105 | .Enrich.WithProperty("Source", "Server") 106 | .WriteTo.Debug( 107 | restrictedToMinimumLevel: logLevel 108 | ); 109 | 110 | string seqUrl = Environment.GetEnvironmentVariable("LSP_SEQ_URL"); 111 | if (!String.IsNullOrWhiteSpace(seqUrl)) 112 | { 113 | loggerConfiguration = loggerConfiguration.WriteTo.Seq(seqUrl, 114 | apiKey: Environment.GetEnvironmentVariable("LSP_SEQ_API_KEY"), 115 | restrictedToMinimumLevel: logLevel 116 | ); 117 | } 118 | 119 | string logFile = Environment.GetEnvironmentVariable("LSP_LOG_FILE"); 120 | if (!String.IsNullOrWhiteSpace(logFile)) 121 | { 122 | string logExtension = Path.GetExtension(logFile); 123 | logFile = Path.GetFullPath( 124 | Path.ChangeExtension(logFile, ".Server" + logExtension) 125 | ); 126 | 127 | loggerConfiguration = loggerConfiguration.WriteTo.File(logFile, 128 | restrictedToMinimumLevel: logLevel 129 | ); 130 | } 131 | 132 | Log.Logger = loggerConfiguration.CreateLogger(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /samples/Server/Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /samples/SingleProcess/Program.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using OmniSharp.Extensions.LanguageServer; 3 | using OmniSharp.Extensions.LanguageServer.Capabilities.Client; 4 | using LSP.Client; 5 | using LSP.Client.Dispatcher; 6 | using LSP.Client.Processes; 7 | using LSP.Client.Protocol; 8 | using Newtonsoft.Json.Linq; 9 | using Serilog; 10 | using Serilog.Events; 11 | using System; 12 | using System.Diagnostics; 13 | using System.IO; 14 | using System.IO.Pipes; 15 | using System.Threading; 16 | using System.Threading.Tasks; 17 | 18 | using LanguageClient = LSP.Client.LanguageClient; 19 | using MSLogging = Microsoft.Extensions.Logging; 20 | 21 | namespace SingleProcess 22 | { 23 | /// 24 | /// A language client and server in a single process, connected via anonymous pipes. 25 | /// 26 | static class Program 27 | { 28 | /// 29 | /// The main program entry-point. 30 | /// 31 | static void Main() 32 | { 33 | SynchronizationContext.SetSynchronizationContext( 34 | new SynchronizationContext() 35 | ); 36 | 37 | ConfigureLogging(); 38 | 39 | try 40 | { 41 | AsyncMain().Wait(); 42 | 43 | Log.Information("All done."); 44 | } 45 | catch (AggregateException unexpectedError) 46 | { 47 | foreach (Exception exception in unexpectedError.Flatten().InnerExceptions) 48 | Log.Error(exception, "Unexpected error."); 49 | } 50 | catch (Exception unexpectedError) 51 | { 52 | Log.Error(unexpectedError, "Unexpected error."); 53 | } 54 | finally 55 | { 56 | Log.CloseAndFlush(); 57 | } 58 | } 59 | 60 | /// 61 | /// The main asynchronous program entry-point. 62 | /// 63 | /// 64 | /// A representing program operation. 65 | /// 66 | static async Task AsyncMain() 67 | { 68 | using (NamedPipeServerProcess serverProcess = new NamedPipeServerProcess("single-process-sample", Log.Logger)) 69 | { 70 | await serverProcess.Start(); 71 | 72 | Task clientTask = RunLanguageClient(serverProcess); 73 | Task serverTask = RunLanguageServer(input: serverProcess.ClientOutputStream, output: serverProcess.ClientInputStream); 74 | 75 | await Task.WhenAll(clientTask, serverTask); 76 | } 77 | } 78 | 79 | /// 80 | /// Run a language client over the specified streams. 81 | /// 82 | /// 83 | /// The used to wire up the client and server streams. 84 | /// 85 | /// 86 | /// A representing the operation. 87 | /// 88 | static async Task RunLanguageClient(NamedPipeServerProcess serverProcess) 89 | { 90 | if (serverProcess == null) 91 | throw new ArgumentNullException(nameof(serverProcess)); 92 | 93 | Log.Information("Starting client..."); 94 | LanguageClient client = new LanguageClient(Log.Logger, serverProcess) 95 | { 96 | ClientCapabilities = 97 | { 98 | Workspace = 99 | { 100 | DidChangeConfiguration = new DidChangeConfigurationCapability 101 | { 102 | DynamicRegistration = false 103 | } 104 | } 105 | } 106 | }; 107 | using (client) 108 | { 109 | // Listen for log messages from the language server. 110 | client.Window.OnLogMessage((message, messageType) => 111 | { 112 | Log.Information("Language server says: [{MessageType:l}] {Message}", messageType, message); 113 | }); 114 | 115 | // Listen for our custom notification from the language server. 116 | client.HandleNotification("dummy/notify", notification => 117 | { 118 | Log.Information("Received dummy notification from language server: {Message}", 119 | notification.Message 120 | ); 121 | }); 122 | 123 | JObject settings = new JObject( 124 | new JProperty("setting1", true), 125 | new JProperty("setting2", "Hello") 126 | ); 127 | 128 | await client.Initialize( 129 | workspaceRoot: @"C:\Foo", 130 | initializationOptions: settings 131 | ); 132 | 133 | Log.Information("Client started."); 134 | 135 | // Update server configuration. 136 | client.Workspace.DidChangeConfiguration(settings); 137 | 138 | // Invoke our custom handler. 139 | await client.SendRequest("dummy", new DummyParams 140 | { 141 | Message = "Hello, world!" 142 | }); 143 | 144 | Log.Information("Shutting down language client..."); 145 | await client.Shutdown(); 146 | Log.Information("Language client has shut down."); 147 | } 148 | } 149 | 150 | /// 151 | /// Run a language server over the specified streams. 152 | /// 153 | /// 154 | /// The input stream. 155 | /// 156 | /// 157 | /// The output stream. 158 | /// 159 | /// 160 | /// A representing the operation. 161 | /// 162 | static async Task RunLanguageServer(Stream input, Stream output) 163 | { 164 | if (input == null) 165 | throw new ArgumentNullException(nameof(input)); 166 | 167 | if (output == null) 168 | throw new ArgumentNullException(nameof(output)); 169 | 170 | Log.Information("Initialising language server..."); 171 | 172 | LanguageServer languageServer = new LanguageServer(input, output, 173 | loggerFactory: new MSLogging.LoggerFactory().AddSerilog(Log.Logger.ForContext()) 174 | ); 175 | 176 | languageServer.AddHandler( 177 | new ConfigurationHandler() 178 | ); 179 | languageServer.AddHandler( 180 | new HoverHandler() 181 | ); 182 | languageServer.AddHandler( 183 | new DummyHandler(languageServer) 184 | ); 185 | 186 | languageServer.OnInitialize(parameters => 187 | { 188 | JToken options = parameters.InitializationOptions as JToken; 189 | Log.Information("Server received initialisation options: {Options}", options?.ToString(Newtonsoft.Json.Formatting.None)); 190 | 191 | return Task.CompletedTask; 192 | }); 193 | 194 | Log.Information("Starting language server..."); 195 | languageServer.Shutdown += shutdownRequested => 196 | { 197 | Log.Information("Language server shutdown (ShutDownRequested={ShutDownRequested}).", shutdownRequested); 198 | }; 199 | languageServer.Exit += exitCode => 200 | { 201 | Log.Information("Language server exit (ExitCode={ExitCode}).", exitCode); 202 | }; 203 | 204 | await languageServer.Initialize(); 205 | 206 | Log.Information("Language server has shut down."); 207 | } 208 | 209 | /// 210 | /// Configure the global logger. 211 | /// 212 | static void ConfigureLogging() 213 | { 214 | LogEventLevel logLevel = 215 | Environment.GetEnvironmentVariable("LSP_VERBOSE_LOGGING") == "1" 216 | ? LogEventLevel.Verbose 217 | : LogEventLevel.Information; 218 | 219 | LoggerConfiguration loggerConfiguration = 220 | new LoggerConfiguration() 221 | .MinimumLevel.Verbose() 222 | .Enrich.WithProperty("ProcessId", Process.GetCurrentProcess().Id) 223 | .Enrich.WithProperty("Source", "SingleProcess") 224 | .WriteTo.Debug( 225 | restrictedToMinimumLevel: logLevel 226 | ); 227 | 228 | string seqUrl = Environment.GetEnvironmentVariable("LSP_SEQ_URL"); 229 | if (!String.IsNullOrWhiteSpace(seqUrl)) 230 | { 231 | loggerConfiguration = loggerConfiguration.WriteTo.Seq(seqUrl, 232 | apiKey: Environment.GetEnvironmentVariable("LSP_SEQ_API_KEY"), 233 | restrictedToMinimumLevel: logLevel 234 | ); 235 | } 236 | 237 | string logFile = Environment.GetEnvironmentVariable("LSP_LOG_FILE"); 238 | if (!String.IsNullOrWhiteSpace(logFile)) 239 | { 240 | string logExtension = Path.GetExtension(logFile); 241 | logFile = Path.GetFullPath( 242 | Path.ChangeExtension(logFile, ".SingleProcess" + logExtension) 243 | ); 244 | 245 | loggerConfiguration = loggerConfiguration.WriteTo.File(logFile, 246 | restrictedToMinimumLevel: logLevel 247 | ); 248 | } 249 | 250 | Log.Logger = loggerConfiguration.CreateLogger(); 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /samples/SingleProcess/SingleProcess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/AssemblyDependencies.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Shell; 2 | 3 | // Register paths for our dependencies to ensure Visual Studio can find them when it loads our extension. 4 | 5 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\OmniSharp.Extensions.JsonRpc.dll")] 6 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\OmniSharp.Extensions.LanguageServerProtocol.dll")] 7 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Serilog.dll")] 8 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Serilog.Sinks.Debug.dll")] 9 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Serilog.Sinks.File.dll")] 10 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Serilog.Sinks.PeriodicBatching.dll")] 11 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Serilog.Sinks.RollingFile.dll")] 12 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Serilog.Sinks.Seq.dll")] 13 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.Diagnostics.Process.dll")] 14 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.IO.dll")] 15 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.Runtime.dll")] 16 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.Reactive.Core.dll")] 17 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.Reactive.Interfaces.dll")] 18 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.Reactive.Linq.dll")] 19 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.Reactive.PlatformServices.dll")] 20 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.Reactive.Windows.Threading.dll")] 21 | [assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\System.ValueTuple.dll")] 22 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/ExtensionPackage.cs: -------------------------------------------------------------------------------- 1 | using LSP.Client; 2 | using Microsoft.VisualStudio; 3 | using Microsoft.VisualStudio.Shell; 4 | using Microsoft.VisualStudio.Shell.Interop; 5 | using Microsoft.VisualStudio.Threading; 6 | using Serilog; 7 | using Serilog.Events; 8 | using System; 9 | using System.Diagnostics; 10 | using System.Runtime.InteropServices; 11 | using System.Threading.Tasks; 12 | using Task = System.Threading.Tasks.Task; 13 | using System.Threading; 14 | using OmniSharp.Extensions.LanguageServer.Models; 15 | 16 | namespace VisualStudioExtension 17 | { 18 | /// 19 | /// The visual studio extension package. 20 | /// 21 | [Guid(PackageGuidString)] 22 | [ProvideAutoLoad(UIContextGuids80.SolutionExists)] 23 | [PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)] 24 | [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About 25 | public sealed class ExtensionPackage 26 | : AsyncPackage 27 | { 28 | /// 29 | /// The package GUID, as a string. 30 | /// 31 | public const string PackageGuidString = "bfe31c89-943f-4106-ad20-5c60f656e9be"; 32 | 33 | /// 34 | /// The GUID of the package's output window pane. 35 | /// 36 | public static readonly Guid PackageOutputPaneGuid = new Guid("9d7abb60-bbe9-4e72-95ff-8cf6df23d5f9"); 37 | 38 | static readonly TaskCompletionSource InitCompletion = new TaskCompletionSource(); 39 | 40 | static ExtensionPackage() 41 | { 42 | LanguageClientInitialized = InitCompletion.Task; 43 | } 44 | 45 | /// 46 | /// Create a new . 47 | /// 48 | public ExtensionPackage() 49 | { 50 | Trace.WriteLine("Enter ExtensionPackage constructor."); 51 | 52 | try 53 | { 54 | Log.Logger = new LoggerConfiguration() 55 | .MinimumLevel.Verbose() 56 | .WriteTo.Debug( 57 | restrictedToMinimumLevel: LogEventLevel.Information 58 | ) 59 | .CreateLogger(); 60 | } 61 | finally 62 | { 63 | Trace.WriteLine("Exit ExtensionPackage constructor."); 64 | } 65 | } 66 | 67 | /// 68 | /// The package's output window pane. 69 | /// 70 | public static IVsOutputWindowPane OutputPane { get; private set; } 71 | 72 | /// 73 | /// The LSP client. 74 | /// 75 | public static LanguageClient LanguageClient { get; private set; } 76 | 77 | /// 78 | /// A representing language client initialisation. 79 | /// 80 | public static Task LanguageClientInitialized { get; private set; } 81 | 82 | /// 83 | /// Dispose of resources being used by the extension package. 84 | /// 85 | /// 86 | /// Explicit disposal? 87 | /// 88 | protected override void Dispose(bool disposing) 89 | { 90 | if (disposing) 91 | { 92 | if (LanguageClient != null) 93 | { 94 | LanguageClient.Dispose(); 95 | LanguageClient = null; 96 | } 97 | } 98 | 99 | base.Dispose(disposing); 100 | } 101 | 102 | /// 103 | /// Called when the package is initialising. 104 | /// 105 | /// 106 | /// A that can be used to cancel the operation. 107 | /// 108 | /// 109 | /// The initialisation progress-reporting facility. 110 | /// 111 | /// 112 | /// A representing package initialisation. 113 | /// 114 | protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) 115 | { 116 | Trace.WriteLine("Enter ExtensionPackage.InitializeAsync."); 117 | 118 | cancellationToken.Register( 119 | () => InitCompletion.TrySetCanceled(cancellationToken) 120 | ); 121 | 122 | await base.InitializeAsync(cancellationToken, progress); 123 | 124 | OutputPane = GetOutputPane(PackageOutputPaneGuid, "LSP Demo"); 125 | OutputPane.Activate(); 126 | 127 | await TaskScheduler.Default; 128 | 129 | try 130 | { 131 | Trace.WriteLine("Creating language service..."); 132 | 133 | LanguageClient = new LanguageClient(Log.Logger, new ProcessStartInfo("dotnet") 134 | { 135 | Arguments = @"""D:\Development\github\tintoy\msbuild-project-tools\out\language-server\MSBuildProjectTools.LanguageServer.Host.dll""", 136 | //Arguments = @"""D:\Development\github\tintoy\dotnet-language-client\samples\Server\bin\Debug\netcoreapp2.0\Server.dll""", 137 | Environment = 138 | { 139 | ["MSBUILD_PROJECT_TOOLS_DIR"] = @"D:\Development\github\tintoy\msbuild-project-tools", 140 | ["MSBUILD_PROJECT_TOOLS_SEQ_URL"] = "http://localhost:5341/", 141 | ["MSBUILD_PROJECT_TOOLS_SEQ_API_KEY"] = "wxEURGakoVuXpIRXyMnt", 142 | ["MSBUILD_PROJECT_TOOLS_VERBOSE_LOGGING"] = "1", 143 | ["LSP_SEQ_URL"] = "http://localhost:5341/", 144 | ["LSP_SEQ_API_KEY"] = "wxEURGakoVuXpIRXyMnt", 145 | ["LSP_VERBOSE_LOGGING"] = "1" 146 | } 147 | }); 148 | LanguageClient.Window.OnLogMessage(LanguageClient_LogMessage); 149 | 150 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 151 | 152 | Trace.WriteLine("Retrieving solution directory..."); 153 | 154 | IVsSolution solution = (IVsSolution)GetService(typeof(SVsSolution)); 155 | 156 | int hr = solution.GetSolutionInfo(out string solutionDir, out _, out _); 157 | ErrorHandler.ThrowOnFailure(hr); 158 | 159 | Trace.WriteLine("Initialising language client..."); 160 | 161 | await TaskScheduler.Default; 162 | 163 | await LanguageClient.Initialize(solutionDir); 164 | 165 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 166 | 167 | Trace.WriteLine("Language client initialised."); 168 | 169 | InitCompletion.TrySetResult(null); 170 | } 171 | catch (Exception languageClientError) 172 | { 173 | Trace.WriteLine(languageClientError); 174 | 175 | InitCompletion.TrySetException(languageClientError); 176 | } 177 | finally 178 | { 179 | Trace.WriteLine("Exit ExtensionPackage.InitializeAsync."); 180 | } 181 | } 182 | 183 | /// 184 | /// Called when the language client receives a log message from the language server. 185 | /// 186 | /// 187 | /// The message text. 188 | /// 189 | /// 190 | /// The message type. 191 | /// 192 | async void LanguageClient_LogMessage(string message, MessageType messageType) 193 | { 194 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 195 | 196 | OutputPane.WriteLine("[{0}] {1}", messageType, message); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tintoy/dotnet-language-client/208f65cf7e5080ed345fd7be19d2d4764d3393ba/samples/VisualStudioExtension/Key.snk -------------------------------------------------------------------------------- /samples/VisualStudioExtension/LspQuickInfoProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Language.Intellisense; 2 | using Microsoft.VisualStudio.Text; 3 | using Microsoft.VisualStudio.Utilities; 4 | using System; 5 | using System.ComponentModel.Composition; 6 | 7 | namespace VisualStudioExtension 8 | { 9 | /// 10 | /// QuickInfo provider for LSP-based Hover information. 11 | /// 12 | [ContentType("XML")] 13 | [Name("LSP Quick Info Controller")] 14 | [Order(After = "Default Quick Info Presenter")] 15 | [Export(typeof(IQuickInfoSourceProvider))] 16 | internal class LspQuickInfoProvider 17 | : IQuickInfoSourceProvider 18 | { 19 | /// 20 | /// Create a new . 21 | /// 22 | [ImportingConstructor] 23 | public LspQuickInfoProvider() 24 | { 25 | } 26 | 27 | /// 28 | /// Create a QuickInfo source for the specified text buffer. 29 | /// 30 | /// 31 | /// The . 32 | /// 33 | /// 34 | /// The , or null if no QuickInfo will be offered. 35 | /// 36 | public IQuickInfoSource TryCreateQuickInfoSource(ITextBuffer textBuffer) 37 | { 38 | if (textBuffer == null) 39 | throw new ArgumentNullException(nameof(textBuffer)); 40 | 41 | return new LspQuickInfoSource(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/LspQuickInfoSource.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using Microsoft.VisualStudio; 3 | using Microsoft.VisualStudio.Language.Intellisense; 4 | using Microsoft.VisualStudio.Shell; 5 | using Microsoft.VisualStudio.Shell.Interop; 6 | using Microsoft.VisualStudio.Text; 7 | using Microsoft.VisualStudio.TextManager.Interop; 8 | using Microsoft.VisualStudio.Threading; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Diagnostics; 12 | using System.Linq; 13 | using System.Threading.Tasks; 14 | using System.Windows; 15 | using System.Windows.Controls; 16 | using System.Windows.Documents; 17 | using System.Windows.Media; 18 | using System.Windows.Shapes; 19 | using Span = Microsoft.VisualStudio.Text.Span; 20 | 21 | namespace VisualStudioExtension 22 | { 23 | using Markdown = Markdown.Xaml.Markdown; 24 | 25 | /// 26 | /// A QuickInfo source that uses Hover content from an LSP language server. 27 | /// 28 | public class LspQuickInfoSource 29 | : IQuickInfoSource 30 | { 31 | /// 32 | /// Create a new . 33 | /// 34 | public LspQuickInfoSource() 35 | { 36 | } 37 | 38 | /// 39 | /// The markdown renderer. 40 | /// 41 | Markdown MarkdownRenderer = new Markdown 42 | { 43 | CodeStyle = new Style 44 | { 45 | TargetType = typeof(Run), 46 | Setters = 47 | { 48 | new Setter 49 | { 50 | Property = TextElement.BackgroundProperty, 51 | Value = GetVSBrush(VsBrushes.AccentPaleKey) 52 | } 53 | } 54 | }, 55 | SeparatorStyle = new Style 56 | { 57 | TargetType = typeof(Line), 58 | Setters = 59 | { 60 | new Setter 61 | { 62 | Property = Shape.StrokeProperty, 63 | Value = GetVSBrush(VsBrushes.DebuggerDataTipActiveBorderKey) 64 | } 65 | } 66 | } 67 | }; 68 | 69 | /// 70 | /// Dispose of resources being used by the . 71 | /// 72 | public void Dispose() 73 | { 74 | } 75 | 76 | /// 77 | /// Augment a QuickInfo session. 78 | /// 79 | /// 80 | /// The target QuickInfo session. 81 | /// 82 | /// 83 | /// The current QuickInfo content. 84 | /// 85 | /// 86 | /// An representing the span of text that the QuickInfo (if any) applies to. 87 | /// 88 | public void AugmentQuickInfoSession(IQuickInfoSession session, IList quickInfoContent, out ITrackingSpan applicableToSpan) 89 | { 90 | if (session == null) 91 | throw new ArgumentNullException(nameof(session)); 92 | 93 | applicableToSpan = null; 94 | 95 | if (!ExtensionPackage.LanguageClientInitialized.IsCompleted) 96 | return; // Language service is not available yet. 97 | 98 | string fileName = session.TextView.TextBuffer.GetFileName(); 99 | if (fileName == null) 100 | return; 101 | 102 | SnapshotPoint? triggerPoint = session.GetTriggerPoint(session.TextView.TextSnapshot); 103 | if (!triggerPoint.HasValue) 104 | return; 105 | 106 | (int line, int column) = triggerPoint.Value.ToLineAndColumn(); 107 | 108 | Hover hover = null; 109 | 110 | // A little bit of thread-fiddling required to do async in Visual Studio: 111 | ThreadHelper.JoinableTaskFactory.Run(async () => 112 | { 113 | await TaskScheduler.Default; 114 | 115 | try 116 | { 117 | await ExtensionPackage.LanguageClientInitialized; 118 | 119 | hover = await ExtensionPackage.LanguageClient.TextDocument.Hover(fileName, line, column); 120 | } 121 | catch (Exception hoverError) 122 | { 123 | Trace.WriteLine(hoverError); 124 | } 125 | }); 126 | 127 | if (hover == null) 128 | return; 129 | 130 | quickInfoContent.Clear(); 131 | 132 | // Quick-and-dirty rendering of Markdown content for display in VS (WPF) UI: 133 | 134 | string hoverContent = String.Join("\n---\n", hover.Contents.Select( 135 | section => section.Value 136 | )); 137 | quickInfoContent.Add(new RichTextBox 138 | { 139 | Document = MarkdownRenderer.Transform(hoverContent), 140 | IsReadOnly = true, 141 | IsReadOnlyCaretVisible = false, 142 | MinWidth = 300, 143 | MinHeight = 50, 144 | Foreground = GetVSBrush(VsBrushes.HelpHowDoIPaneTextKey), 145 | Background = Brushes.Transparent, 146 | BorderBrush = Brushes.Transparent, 147 | Focusable = false 148 | }); 149 | 150 | Span span = session.TextView.TextSnapshot.GetSpan(hover.Range); 151 | 152 | applicableToSpan = session.TextView.TextSnapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeInclusive); 153 | } 154 | 155 | /// 156 | /// Get the specified from Visual Studio application resources. 157 | /// 158 | /// 159 | /// An object from used as a resource key. 160 | /// 161 | /// 162 | /// The , or null if no resource was found with the specified key. 163 | /// 164 | static Brush GetVSBrush(object brushKey) => (Brush)Application.Current.Resources[brushKey]; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("VisualStudioExtension")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("VisualStudioExtension")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // Version information for an assembly consists of the following four values: 23 | // 24 | // Major Version 25 | // Minor Version 26 | // Build Number 27 | // Revision 28 | // 29 | // You can specify all the values or you can default the Build and Revision Numbers 30 | // by using the '*' as shown below: 31 | // [assembly: AssemblyVersion("1.0.*")] 32 | [assembly: AssemblyVersion("1.0.0.0")] 33 | [assembly: AssemblyFileVersion("1.0.0.0")] 34 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/README.md: -------------------------------------------------------------------------------- 1 | ## Errors in VS Code (OmniSharp) 2 | 3 | Because this project uses legacy-style `packages.config`, OmniSharp will report errors regarding `netstandard, Version=2.0.0.0`. You can safely ignore them. 4 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/Resources/ExtensionPackage.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tintoy/dotnet-language-client/208f65cf7e5080ed345fd7be19d2d4764d3393ba/samples/VisualStudioExtension/Resources/ExtensionPackage.ico -------------------------------------------------------------------------------- /samples/VisualStudioExtension/TextViewCreationListener.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.Editor; 2 | using Microsoft.VisualStudio.Text.Editor; 3 | using Microsoft.VisualStudio.Utilities; 4 | using System; 5 | using System.ComponentModel.Composition; 6 | using System.Threading.Tasks; 7 | using Microsoft.VisualStudio.TextManager.Interop; 8 | using Microsoft.VisualStudio.Text; 9 | using Microsoft.VisualStudio.Shell; 10 | using Microsoft.VisualStudio.Threading; 11 | 12 | namespace VisualStudioExtension 13 | { 14 | /// 15 | /// Listens for creation of text views with the XML content type, and notifies the language client (via TextDocument.DidOpen). 16 | /// 17 | [ContentType("xml")] 18 | [Export(typeof(IVsTextViewCreationListener))] 19 | [Name("token completion handler")] 20 | [TextViewRole(PredefinedTextViewRoles.Editable)] 21 | class XmlTextViewCreationListener 22 | : IVsTextViewCreationListener 23 | { 24 | /// 25 | /// The editor adapter factory service. 26 | /// 27 | [Import] 28 | public IVsEditorAdaptersFactoryService EditorAdapters { get; set; } 29 | 30 | /// 31 | /// Called when a text view is created. 32 | /// 33 | /// 34 | /// An representing the text view. 35 | /// 36 | public async void VsTextViewCreated(IVsTextView textViewAdapter) 37 | { 38 | if (textViewAdapter == null) 39 | return; 40 | 41 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 42 | 43 | IWpfTextView textView = EditorAdapters.GetWpfTextView(textViewAdapter); 44 | if (textView == null) 45 | return; 46 | 47 | ITextBuffer buffer = textView.TextBuffer; 48 | if (buffer == null) 49 | return; 50 | 51 | string contentType = buffer.ContentType.TypeName; 52 | 53 | string fileName = buffer.GetFileName(); 54 | if (String.IsNullOrWhiteSpace(fileName)) 55 | return; 56 | 57 | textView.Closed += async (sender, args) => 58 | { 59 | await ExtensionPackage.LanguageClientInitialized; 60 | 61 | ExtensionPackage.LanguageClient.TextDocument.DidClose(fileName); 62 | }; 63 | 64 | await TaskScheduler.Default; 65 | await ExtensionPackage.LanguageClientInitialized; 66 | 67 | ExtensionPackage.LanguageClient.TextDocument.DidOpen(fileName, languageId: contentType.ToLower()); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/ThreadAffinitiveSynchronizationContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Runtime.ExceptionServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace VisualStudioExtension 10 | { 11 | /// 12 | /// A synchronisation context that runs all calls scheduled on it (via ) on a single thread. 13 | /// 14 | [DebuggerNonUserCode, DebuggerStepThrough] 15 | public sealed class ThreadAffinitiveSynchronizationContext 16 | : SynchronizationContext, IDisposable 17 | { 18 | #region Instance data 19 | 20 | /// 21 | /// A blocking collection (effectively a queue) of work items to execute, consisting of callback delegates and their callback state (if any). 22 | /// 23 | BlockingCollection> _workItemQueue = new BlockingCollection>(); 24 | 25 | #endregion // Instance data 26 | 27 | #region Construction / disposal 28 | 29 | /// 30 | /// Create a new thread-affinitive synchronisation context. 31 | /// 32 | public ThreadAffinitiveSynchronizationContext() 33 | { 34 | } 35 | 36 | /// 37 | /// Dispose of resources being used by the synchronisation context. 38 | /// 39 | public void Dispose() 40 | { 41 | if (_workItemQueue != null) 42 | { 43 | _workItemQueue.Dispose(); 44 | _workItemQueue = null; 45 | } 46 | } 47 | 48 | /// 49 | /// Check if the synchronisation context has been disposed. 50 | /// 51 | void CheckDisposed() 52 | { 53 | if (_workItemQueue == null) 54 | throw new ObjectDisposedException(GetType().Name); 55 | } 56 | 57 | #endregion // Construction / disposal 58 | 59 | #region Public methods 60 | 61 | /// 62 | /// Run the message pump for the callback queue on the current thread. 63 | /// 64 | public void RunMessagePump() 65 | { 66 | CheckDisposed(); 67 | 68 | KeyValuePair workItem; 69 | while (_workItemQueue.TryTake(out workItem, Timeout.InfiniteTimeSpan)) 70 | { 71 | workItem.Key(workItem.Value); 72 | 73 | // Has the synchronisation context been disposed? 74 | if (_workItemQueue == null) 75 | break; 76 | } 77 | } 78 | 79 | /// 80 | /// Terminate the message pump once all callbacks have completed. 81 | /// 82 | public void TerminateMessagePump() 83 | { 84 | CheckDisposed(); 85 | 86 | _workItemQueue.CompleteAdding(); 87 | } 88 | 89 | #endregion // Public methods 90 | 91 | #region SynchronizationContext overrides 92 | 93 | /// 94 | /// Dispatch an asynchronous message to the synchronization context. 95 | /// 96 | /// 97 | /// The delegate to call in the synchronisation context. 98 | /// 99 | /// 100 | /// Optional state data passed to the callback. 101 | /// 102 | /// 103 | /// The message pump has already been started, and then terminated by calling . 104 | /// 105 | public override void Post(SendOrPostCallback callback, object callbackState) 106 | { 107 | if (callback == null) 108 | throw new ArgumentNullException(nameof(callback)); 109 | 110 | CheckDisposed(); 111 | 112 | try 113 | { 114 | _workItemQueue.Add( 115 | new KeyValuePair( 116 | key: callback, 117 | value: callbackState 118 | ) 119 | ); 120 | } 121 | catch (InvalidOperationException eMessagePumpAlreadyTerminated) 122 | { 123 | throw new InvalidOperationException( 124 | "Cannot enqueue the specified callback because the synchronisation context's message pump has already been terminated.", 125 | eMessagePumpAlreadyTerminated 126 | ); 127 | } 128 | } 129 | 130 | #endregion // SynchronizationContext overrides 131 | 132 | #region Static implementation 133 | 134 | /// 135 | /// Run an asynchronous operation using the current thread as its synchronisation context. 136 | /// 137 | /// 138 | /// A delegate representing the asynchronous operation to run. 139 | /// 140 | public static void RunSynchronized(Func asyncOperation) 141 | { 142 | if (asyncOperation == null) 143 | throw new ArgumentNullException(nameof(asyncOperation)); 144 | 145 | SynchronizationContext savedContext = Current; 146 | try 147 | { 148 | using (ThreadAffinitiveSynchronizationContext synchronizationContext = new ThreadAffinitiveSynchronizationContext()) 149 | { 150 | SetSynchronizationContext(synchronizationContext); 151 | 152 | Task rootOperationTask = asyncOperation(); 153 | if (rootOperationTask == null) 154 | throw new InvalidOperationException("The asynchronous operation delegate cannot return null."); 155 | 156 | rootOperationTask 157 | .ContinueWith( 158 | operationTask => 159 | synchronizationContext.TerminateMessagePump(), 160 | scheduler: 161 | TaskScheduler.Default 162 | ); 163 | 164 | synchronizationContext.RunMessagePump(); 165 | 166 | try 167 | { 168 | rootOperationTask 169 | .GetAwaiter() 170 | .GetResult(); 171 | } 172 | catch (AggregateException eWaitForTask) // The TPL will almost always wrap an AggregateException around any exception thrown by the async operation. 173 | { 174 | // Is this just a wrapped exception? 175 | AggregateException flattenedAggregate = eWaitForTask.Flatten(); 176 | if (flattenedAggregate.InnerExceptions.Count != 1) 177 | throw; // Nope, genuine aggregate. 178 | 179 | // Yep, so rethrow (preserving original stack-trace). 180 | ExceptionDispatchInfo 181 | .Capture( 182 | flattenedAggregate 183 | .InnerExceptions[0] 184 | ) 185 | .Throw(); 186 | } 187 | } 188 | } 189 | finally 190 | { 191 | SetSynchronizationContext(savedContext); 192 | } 193 | } 194 | 195 | /// 196 | /// Run an asynchronous operation using the current thread as its synchronisation context. 197 | /// 198 | /// 199 | /// The operation result type. 200 | /// 201 | /// 202 | /// A delegate representing the asynchronous operation to run. 203 | /// 204 | /// 205 | /// The operation result. 206 | /// 207 | public static TResult RunSynchronized(Func> asyncOperation) 208 | { 209 | if (asyncOperation == null) 210 | throw new ArgumentNullException(nameof(asyncOperation)); 211 | 212 | SynchronizationContext savedContext = Current; 213 | try 214 | { 215 | using (ThreadAffinitiveSynchronizationContext synchronizationContext = new ThreadAffinitiveSynchronizationContext()) 216 | { 217 | SetSynchronizationContext(synchronizationContext); 218 | 219 | Task rootOperationTask = asyncOperation(); 220 | if (rootOperationTask == null) 221 | throw new InvalidOperationException("The asynchronous operation delegate cannot return null."); 222 | 223 | rootOperationTask 224 | .ContinueWith( 225 | operationTask => 226 | synchronizationContext.TerminateMessagePump(), 227 | scheduler: 228 | TaskScheduler.Default 229 | ); 230 | 231 | synchronizationContext.RunMessagePump(); 232 | 233 | try 234 | { 235 | return 236 | rootOperationTask 237 | .GetAwaiter() 238 | .GetResult(); 239 | } 240 | catch (AggregateException eWaitForTask) // The TPL will almost always wrap an AggregateException around any exception thrown by the async operation. 241 | { 242 | // Is this just a wrapped exception? 243 | AggregateException flattenedAggregate = eWaitForTask.Flatten(); 244 | if (flattenedAggregate.InnerExceptions.Count != 1) 245 | throw; // Nope, genuine aggregate. 246 | 247 | // Yep, so rethrow (preserving original stack-trace). 248 | ExceptionDispatchInfo 249 | .Capture( 250 | flattenedAggregate 251 | .InnerExceptions[0] 252 | ) 253 | .Throw(); 254 | 255 | throw; // Never reached. 256 | } 257 | } 258 | } 259 | finally 260 | { 261 | SetSynchronizationContext(savedContext); 262 | } 263 | } 264 | 265 | #endregion // Static implementation 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/VSPackage.resx: -------------------------------------------------------------------------------- 1 |  2 | 12 | 13 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | text/microsoft-resx 120 | 121 | 122 | 2.0 123 | 124 | 125 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 126 | 127 | 128 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 129 | 130 | 131 | 132 | ExtensionPackage Extension 133 | 134 | 135 | ExtensionPackage Visual Studio Extension Detailed Info 136 | 137 | 138 | Resources\ExtensionPackage.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 139 | 140 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/VisualStudioApiExtensions.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using Microsoft.VisualStudio; 3 | using Microsoft.VisualStudio.Shell.Interop; 4 | using Microsoft.VisualStudio.Text; 5 | using Microsoft.VisualStudio.TextManager.Interop; 6 | using System; 7 | 8 | namespace VisualStudioExtension 9 | { 10 | /// 11 | /// Extension methods for the VS API. 12 | /// 13 | static class VisualStudioApiExtensions 14 | { 15 | /// 16 | /// Write a line of text to the output pane. 17 | /// 18 | /// 19 | /// The output window pane. 20 | /// 21 | /// 22 | /// The message to write. 23 | /// 24 | public static void WriteLine(this IVsOutputWindowPane outputPane, string message) 25 | { 26 | if (outputPane == null) 27 | throw new ArgumentNullException(nameof(outputPane)); 28 | 29 | if (message == null) 30 | throw new ArgumentNullException(nameof(message)); 31 | 32 | outputPane.OutputString(message + "\n"); 33 | } 34 | 35 | /// 36 | /// Write a line of text to the output pane. 37 | /// 38 | /// 39 | /// The output window pane. 40 | /// 41 | /// 42 | /// The message or message-format specifier to write. 43 | /// 44 | /// 45 | /// Optional message-format arguments. 46 | /// 47 | public static void WriteLine(this IVsOutputWindowPane outputPane, string messageOrFormat, params object[] formatArguments) 48 | { 49 | if (outputPane == null) 50 | throw new ArgumentNullException(nameof(outputPane)); 51 | 52 | if (String.IsNullOrWhiteSpace(messageOrFormat)) 53 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(messageOrFormat)}.", nameof(messageOrFormat)); 54 | 55 | outputPane.WriteLine( 56 | String.Format(messageOrFormat, formatArguments) 57 | ); 58 | } 59 | 60 | /// 61 | /// Attempt to determine the name of the file represented by the specified text buffer. 62 | /// 63 | /// 64 | /// The text buffer. 65 | /// 66 | /// 67 | /// The file name, or null if the file name could not be determined. 68 | /// 69 | public static string GetFileName(this ITextBuffer textBuffer) 70 | { 71 | if (textBuffer == null) 72 | throw new ArgumentNullException(nameof(textBuffer)); 73 | 74 | IVsTextBuffer bufferAdapter; 75 | textBuffer.Properties.TryGetProperty(typeof(IVsTextBuffer), out bufferAdapter); 76 | if (bufferAdapter == null) 77 | return null; 78 | 79 | IPersistFileFormat persistFileFormat = bufferAdapter as IPersistFileFormat; 80 | if (persistFileFormat == null) 81 | return null; 82 | 83 | string fileName = null; 84 | 85 | int hr = persistFileFormat.GetCurFile(out fileName, out _); 86 | ErrorHandler.ThrowOnFailure(hr); 87 | 88 | return fileName; 89 | } 90 | 91 | /// 92 | /// Get a representing the specified position in the . 93 | /// 94 | /// 95 | /// The . 96 | /// 97 | /// 98 | /// The target LSP . 99 | /// 100 | /// 101 | /// The . 102 | /// 103 | public static SnapshotPoint GetPoint(this ITextSnapshot snapshot, Position position) 104 | { 105 | if (snapshot == null) 106 | throw new ArgumentNullException(nameof(snapshot)); 107 | 108 | if (position == null) 109 | throw new ArgumentNullException(nameof(position)); 110 | 111 | return snapshot.GetPoint(position.Line, position.Character); 112 | } 113 | 114 | /// 115 | /// Get a representing the specified (0-based) line and column in the . 116 | /// 117 | /// 118 | /// The . 119 | /// 120 | /// 121 | /// The target line (0-based). 122 | /// 123 | /// 124 | /// The target column (0-based). 125 | /// 126 | /// 127 | /// The . 128 | /// 129 | public static SnapshotPoint GetPoint(this ITextSnapshot snapshot, long line, long column) 130 | { 131 | if (snapshot == null) 132 | throw new ArgumentNullException(nameof(snapshot)); 133 | 134 | ITextSnapshotLine snapshotLine = snapshot.GetLineFromLineNumber((int)line); 135 | 136 | return snapshotLine.Start.Add((int)column); 137 | } 138 | 139 | /// 140 | /// Convert the to a (0-based) line and column number. 141 | /// 142 | /// 143 | /// The . 144 | /// 145 | /// 146 | /// The line and column number. 147 | /// 148 | public static (int line, int column) ToLineAndColumn(this SnapshotPoint snapshotPoint) 149 | { 150 | var line = snapshotPoint.GetContainingLine(); 151 | int lineNumber = line.LineNumber; 152 | int columnNumber = snapshotPoint.Subtract(line.Start).Position; 153 | 154 | return (lineNumber, columnNumber); 155 | } 156 | 157 | /// 158 | /// Get a representing the specified range within the . 159 | /// 160 | /// 161 | /// The . 162 | /// 163 | /// 164 | /// The target LSP . 165 | /// 166 | /// 167 | /// The . 168 | /// 169 | public static Span GetSpan(this ITextSnapshot snapshot, Range range) 170 | { 171 | if (snapshot == null) 172 | throw new ArgumentNullException(nameof(snapshot)); 173 | 174 | if (range == null) 175 | throw new ArgumentNullException(nameof(range)); 176 | 177 | SnapshotPoint start = snapshot.GetPoint(range.Start); 178 | SnapshotPoint end = snapshot.GetPoint(range.End); 179 | 180 | return new Span( 181 | start: start.Position, 182 | length: end.Position - start.Position 183 | ); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /samples/VisualStudioExtension/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | LSP Client Demo 6 | A demonstration of using an LSP-compliant language service from Visual Studio. 7 | A demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | IDE0016 4 | true 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/LSP.Client/Clients/TextDocumentClient.Completions.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace LSP.Client.Clients 7 | { 8 | using Utilities; 9 | 10 | /// 11 | /// Client for the LSP Text Document API. 12 | /// 13 | public partial class TextDocumentClient 14 | { 15 | /// 16 | /// Request completions at the specified document position. 17 | /// 18 | /// 19 | /// The full file-system path of the text document. 20 | /// 21 | /// 22 | /// The target line (0-based). 23 | /// 24 | /// 25 | /// The target column (0-based). 26 | /// 27 | /// 28 | /// An optional that can be used to cancel the request. 29 | /// 30 | /// 31 | /// A that resolves to the completions or null if no completions are available at the specified position. 32 | /// 33 | public Task Completions(string filePath, int line, int column, CancellationToken cancellationToken = default(CancellationToken)) 34 | { 35 | if (String.IsNullOrWhiteSpace(filePath)) 36 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(filePath)}.", nameof(filePath)); 37 | 38 | return PositionalRequest("textDocument/completion", filePath, line, column, cancellationToken); 39 | } 40 | 41 | /// 42 | /// Request completions at the specified document position. 43 | /// 44 | /// 45 | /// The document URI. 46 | /// 47 | /// 48 | /// The target line (0-based). 49 | /// 50 | /// 51 | /// The target column (0-based). 52 | /// 53 | /// 54 | /// An optional that can be used to cancel the request. 55 | /// 56 | /// 57 | /// A that resolves to the completions or null if no completions are available at the specified position. 58 | /// 59 | public Task Completions(Uri documentUri, int line, int column, CancellationToken cancellationToken = default(CancellationToken)) 60 | { 61 | return PositionalRequest("textDocument/completion", documentUri, line, column, cancellationToken); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/LSP.Client/Clients/TextDocumentClient.Diagnostics.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using System; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSP.Client.Clients 8 | { 9 | using System.Collections.Generic; 10 | using Utilities; 11 | 12 | /// 13 | /// Client for the LSP Text Document API. 14 | /// 15 | public partial class TextDocumentClient 16 | { 17 | /// 18 | /// Register a handler for diagnostics published by the language server. 19 | /// 20 | /// 21 | /// A that is called to publish the diagnostics. 22 | /// 23 | /// 24 | /// An representing the registration. 25 | /// 26 | /// 27 | /// The diagnostics should replace any previously published diagnostics for the specified document. 28 | /// 29 | public IDisposable OnPublishDiagnostics(PublishDiagnosticsHandler handler) 30 | { 31 | if (handler == null) 32 | throw new ArgumentNullException(nameof(handler)); 33 | 34 | return Client.HandleNotification("textDocument/publishDiagnostics", notification => 35 | { 36 | if (notification.Diagnostics == null) 37 | return; // Invalid notification. 38 | 39 | List diagnostics = new List(); 40 | if (notification.Diagnostics != null) 41 | diagnostics.AddRange(notification.Diagnostics); 42 | 43 | handler(notification.Uri, diagnostics); 44 | }); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/LSP.Client/Clients/TextDocumentClient.Hover.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using System; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSP.Client.Clients 8 | { 9 | using Utilities; 10 | 11 | /// 12 | /// Client for the LSP Text Document API. 13 | /// 14 | public partial class TextDocumentClient 15 | { 16 | /// 17 | /// Request hover information at the specified document position. 18 | /// 19 | /// 20 | /// The full file-system path of the text document. 21 | /// 22 | /// 23 | /// The target line (0-based). 24 | /// 25 | /// 26 | /// The target column (0-based). 27 | /// 28 | /// 29 | /// An optional that can be used to cancel the request. 30 | /// 31 | /// 32 | /// A that resolves to the hover information or null if no hover information is available at the specified position. 33 | /// 34 | public Task Hover(string filePath, int line, int column, CancellationToken cancellationToken = default(CancellationToken)) 35 | { 36 | return PositionalRequest("textDocument/hover", filePath, line, column, cancellationToken); 37 | } 38 | 39 | /// 40 | /// Request hover information at the specified document position. 41 | /// 42 | /// 43 | /// The document URI. 44 | /// 45 | /// 46 | /// The target line (0-based). 47 | /// 48 | /// 49 | /// The target column (0-based). 50 | /// 51 | /// 52 | /// An optional that can be used to cancel the request. 53 | /// 54 | /// 55 | /// A that resolves to the hover information or null if no hover information is available at the specified position. 56 | /// 57 | public Task Hover(Uri documentUri, int line, int column, CancellationToken cancellationToken = default(CancellationToken)) 58 | { 59 | return PositionalRequest("textDocument/hover", documentUri, line, column, cancellationToken); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/LSP.Client/Clients/TextDocumentClient.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using System; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSP.Client.Clients 8 | { 9 | using Utilities; 10 | 11 | /// 12 | /// Client for the LSP Text Document API. 13 | /// 14 | public partial class TextDocumentClient 15 | { 16 | /// 17 | /// Create a new . 18 | /// 19 | /// 20 | /// The language client providing the API. 21 | /// 22 | public TextDocumentClient(LanguageClient client) 23 | { 24 | if (client == null) 25 | throw new ArgumentNullException(nameof(client)); 26 | 27 | Client = client; 28 | } 29 | 30 | /// 31 | /// The language client providing the API. 32 | /// 33 | public LanguageClient Client { get; } 34 | 35 | /// 36 | /// Make a request to the server for the specified document position. 37 | /// 38 | /// 39 | /// The response payload type. 40 | /// 41 | /// 42 | /// The name of the operation to invoke. 43 | /// 44 | /// 45 | /// The file-system path of the target document. 46 | /// 47 | /// 48 | /// The target line numer (0-based). 49 | /// 50 | /// 51 | /// The target column (0-based). 52 | /// 53 | /// 54 | /// A cancellation token that can be used to cancel the request. 55 | /// 56 | /// 57 | /// A representing the request. 58 | /// 59 | Task PositionalRequest(string method, string filePath, int line, int column, CancellationToken cancellationToken) 60 | { 61 | if (String.IsNullOrWhiteSpace(method)) 62 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 63 | 64 | if (String.IsNullOrWhiteSpace(filePath)) 65 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(filePath)}.", nameof(filePath)); 66 | 67 | Uri documentUri = DocumentUri.FromFileSystemPath(filePath); 68 | 69 | return PositionalRequest(method, documentUri, line, column, cancellationToken); 70 | } 71 | 72 | /// 73 | /// Make a request to the server for the specified document position. 74 | /// 75 | /// 76 | /// The response payload type. 77 | /// 78 | /// 79 | /// The name of the operation to invoke. 80 | /// 81 | /// 82 | /// The URI of the target document. 83 | /// 84 | /// 85 | /// The target line numer (0-based). 86 | /// 87 | /// 88 | /// The target column (0-based). 89 | /// 90 | /// 91 | /// A cancellation token that can be used to cancel the request. 92 | /// 93 | /// 94 | /// A representing the request. 95 | /// 96 | async Task PositionalRequest(string method, Uri documentUri, int line, int column, CancellationToken cancellationToken) 97 | { 98 | if (String.IsNullOrWhiteSpace(method)) 99 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 100 | 101 | if (documentUri == null) 102 | throw new ArgumentNullException(nameof(documentUri)); 103 | 104 | var request = new TextDocumentPositionParams 105 | { 106 | TextDocument = new TextDocumentItem 107 | { 108 | Uri = documentUri 109 | }, 110 | Position = new Position 111 | { 112 | Line = line, 113 | Character = column 114 | } 115 | }; 116 | 117 | return await Client.SendRequest(method, request, cancellationToken).ConfigureAwait(false); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/LSP.Client/Clients/WindowClient.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using System; 3 | 4 | namespace LSP.Client.Clients 5 | { 6 | /// 7 | /// Client for the LSP Window API. 8 | /// 9 | public class WindowClient 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The language client providing the API. 16 | /// 17 | public WindowClient(LanguageClient client) 18 | { 19 | if (client == null) 20 | throw new ArgumentNullException(nameof(client)); 21 | 22 | Client = client; 23 | } 24 | 25 | /// 26 | /// The language client providing the API. 27 | /// 28 | public LanguageClient Client { get; } 29 | 30 | /// 31 | /// Register a handler for "window/logMessage" notifications from the server. 32 | /// 33 | /// 34 | /// The that will be called for each log message. 35 | /// 36 | /// 37 | /// An representing the registration. 38 | /// 39 | public IDisposable OnLogMessage(LogMessageHandler handler) 40 | { 41 | if (handler == null) 42 | throw new ArgumentNullException(nameof(handler)); 43 | 44 | return Client.HandleNotification("window/logMessage", 45 | notification => handler(notification.Message, notification.Type) 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/LSP.Client/Clients/WorkspaceClient.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | 4 | namespace LSP.Client.Clients 5 | { 6 | /// 7 | /// Client for the LSP Workspace API. 8 | /// 9 | public class WorkspaceClient 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The language client providing the API. 16 | /// 17 | public WorkspaceClient(LanguageClient client) 18 | { 19 | if (client == null) 20 | throw new ArgumentNullException(nameof(client)); 21 | 22 | Client = client; 23 | } 24 | 25 | /// 26 | /// The language client providing the API. 27 | /// 28 | public LanguageClient Client { get; } 29 | 30 | /// 31 | /// Notify the language server that workspace configuration has changed. 32 | /// 33 | /// 34 | /// A representing the workspace configuration (or a subset thereof). 35 | /// 36 | public void DidChangeConfiguration(JObject configuration) 37 | { 38 | if (configuration == null) 39 | throw new ArgumentNullException(nameof(configuration)); 40 | 41 | Client.SendNotification("workspace/didChangeConfiguration", new JObject( 42 | new JProperty("settings", configuration) 43 | )); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/LSP.Client/Dispatcher/LspDispatcher.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Reactive.Disposables; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSP.Client.Dispatcher 9 | { 10 | using Handlers; 11 | 12 | /// 13 | /// Dispatches requests and notifications from a language server to a language client. 14 | /// 15 | public class LspDispatcher 16 | { 17 | /// 18 | /// Invokers for registered handlers. 19 | /// 20 | readonly ConcurrentDictionary _handlers = new ConcurrentDictionary(); 21 | 22 | /// 23 | /// Create a new . 24 | /// 25 | public LspDispatcher() 26 | { 27 | } 28 | 29 | /// 30 | /// Register a handler invoker. 31 | /// 32 | /// 33 | /// The handler. 34 | /// 35 | /// 36 | /// An representing the registration. 37 | /// 38 | public IDisposable RegisterHandler(IHandler handler) 39 | { 40 | if (handler == null) 41 | throw new ArgumentNullException(nameof(handler)); 42 | 43 | string method = handler.Method; 44 | 45 | if (!_handlers.TryAdd(method, handler)) 46 | throw new InvalidOperationException($"There is already a handler registered for method '{handler.Method}'."); 47 | 48 | return Disposable.Create( 49 | () => _handlers.TryRemove(method, out _) 50 | ); 51 | } 52 | 53 | /// 54 | /// Attempt to handle an empty notification. 55 | /// 56 | /// 57 | /// The notification method name. 58 | /// 59 | /// 60 | /// true, if an empty notification handler was registered for specified method; otherwise, false. 61 | /// 62 | public async Task TryHandleEmptyNotification(string method) 63 | { 64 | if (String.IsNullOrWhiteSpace(method)) 65 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 66 | 67 | if (_handlers.TryGetValue(method, out IHandler handler) && handler is IInvokeEmptyNotificationHandler emptyNotificationHandler) 68 | { 69 | await emptyNotificationHandler.Invoke(); 70 | 71 | return true; 72 | } 73 | 74 | return false; 75 | } 76 | 77 | /// 78 | /// Attempt to handle a notification. 79 | /// 80 | /// 81 | /// The notification method name. 82 | /// 83 | /// 84 | /// The notification message. 85 | /// 86 | /// 87 | /// true, if a notification handler was registered for specified method; otherwise, false. 88 | /// 89 | public async Task TryHandleNotification(string method, JObject notification) 90 | { 91 | if (String.IsNullOrWhiteSpace(method)) 92 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 93 | 94 | if (_handlers.TryGetValue(method, out IHandler handler) && handler is IInvokeNotificationHandler notificationHandler) 95 | { 96 | await notificationHandler.Invoke(notification); 97 | 98 | return true; 99 | } 100 | 101 | return false; 102 | } 103 | 104 | /// 105 | /// Attempt to handle a request. 106 | /// 107 | /// 108 | /// The request method name. 109 | /// 110 | /// 111 | /// The request message. 112 | /// 113 | /// 114 | /// A that can be used to cancel the operation. 115 | /// 116 | /// 117 | /// If a registered handler was found, a representing the operation; otherwise, null. 118 | /// 119 | public Task TryHandleRequest(string method, JObject request, CancellationToken cancellationToken) 120 | { 121 | if (String.IsNullOrWhiteSpace(method)) 122 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 123 | 124 | if (_handlers.TryGetValue(method, out IHandler handler) && handler is IInvokeRequestHandler requestHandler) 125 | return requestHandler.Invoke(request, cancellationToken); 126 | 127 | return null; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/LSP.Client/Dispatcher/LspDispatcherExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LSP.Client.Dispatcher 4 | { 5 | using Handlers; 6 | 7 | /// 8 | /// Extension methods for enabling various styles of handler registration. 9 | /// 10 | public static class LspDispatcherExtensions 11 | { 12 | /// 13 | /// Register a handler for empty notifications. 14 | /// 15 | /// 16 | /// The . 17 | /// 18 | /// 19 | /// The name of the notification method to handle. 20 | /// 21 | /// 22 | /// A delegate that implements the handler. 23 | /// 24 | /// 25 | /// An representing the registration. 26 | /// 27 | public static IDisposable HandleEmptyNotification(this LspDispatcher clientDispatcher, string method, EmptyNotificationHandler handler) 28 | { 29 | if (clientDispatcher == null) 30 | throw new ArgumentNullException(nameof(clientDispatcher)); 31 | 32 | if (String.IsNullOrWhiteSpace(method)) 33 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 34 | 35 | if (handler == null) 36 | throw new ArgumentNullException(nameof(handler)); 37 | 38 | return clientDispatcher.RegisterHandler( 39 | new DelegateEmptyNotificationHandler(method, handler) 40 | ); 41 | } 42 | 43 | /// 44 | /// Register a handler for notifications. 45 | /// 46 | /// 47 | /// The notification message type. 48 | /// 49 | /// 50 | /// The . 51 | /// 52 | /// 53 | /// The name of the notification method to handle. 54 | /// 55 | /// 56 | /// A delegate that implements the handler. 57 | /// 58 | /// 59 | /// An representing the registration. 60 | /// 61 | public static IDisposable HandleNotification(this LspDispatcher clientDispatcher, string method, NotificationHandler handler) 62 | { 63 | if (clientDispatcher == null) 64 | throw new ArgumentNullException(nameof(clientDispatcher)); 65 | 66 | if (String.IsNullOrWhiteSpace(method)) 67 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 68 | 69 | if (handler == null) 70 | throw new ArgumentNullException(nameof(handler)); 71 | 72 | return clientDispatcher.RegisterHandler( 73 | new DelegateNotificationHandler(method, handler) 74 | ); 75 | } 76 | 77 | /// 78 | /// Register a handler for requests. 79 | /// 80 | /// 81 | /// The request message type. 82 | /// 83 | /// 84 | /// The . 85 | /// 86 | /// 87 | /// The name of the request method to handle. 88 | /// 89 | /// 90 | /// A delegate that implements the handler. 91 | /// 92 | /// 93 | /// An representing the registration. 94 | /// 95 | public static IDisposable HandleRequest(this LspDispatcher clientDispatcher, string method, RequestHandler handler) 96 | { 97 | if (clientDispatcher == null) 98 | throw new ArgumentNullException(nameof(clientDispatcher)); 99 | 100 | if (String.IsNullOrWhiteSpace(method)) 101 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 102 | 103 | if (handler == null) 104 | throw new ArgumentNullException(nameof(handler)); 105 | 106 | return clientDispatcher.RegisterHandler( 107 | new DelegateRequestHandler(method, handler) 108 | ); 109 | } 110 | 111 | /// 112 | /// Register a handler for requests. 113 | /// 114 | /// 115 | /// The request message type. 116 | /// 117 | /// 118 | /// The response message type. 119 | /// 120 | /// 121 | /// The . 122 | /// 123 | /// 124 | /// The name of the request method to handle. 125 | /// 126 | /// 127 | /// A delegate that implements the handler. 128 | /// 129 | /// 130 | /// An representing the registration. 131 | /// 132 | public static IDisposable HandleRequest(this LspDispatcher clientDispatcher, string method, RequestHandler handler) 133 | { 134 | if (clientDispatcher == null) 135 | throw new ArgumentNullException(nameof(clientDispatcher)); 136 | 137 | if (String.IsNullOrWhiteSpace(method)) 138 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 139 | 140 | if (handler == null) 141 | throw new ArgumentNullException(nameof(handler)); 142 | 143 | return clientDispatcher.RegisterHandler( 144 | new DelegateRequestResponseHandler(method, handler) 145 | ); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/LSP.Client/HandlerDelegates.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSP.Client 8 | { 9 | /// 10 | /// A handler for empty notifications. 11 | /// 12 | /// 13 | /// A representing the operation. 14 | /// 15 | public delegate void EmptyNotificationHandler(); 16 | 17 | /// 18 | /// A handler for notifications. 19 | /// 20 | /// 21 | /// The notification message type. 22 | /// 23 | /// 24 | /// The notification message. 25 | /// 26 | public delegate void NotificationHandler(TNotification notification); 27 | 28 | /// 29 | /// A handler for requests. 30 | /// 31 | /// 32 | /// The request message type. 33 | /// 34 | /// 35 | /// The request message. 36 | /// 37 | /// 38 | /// A that can be used to cancel the operation. 39 | /// 40 | /// 41 | /// A representing the operation. 42 | /// 43 | public delegate Task RequestHandler(TRequest request, CancellationToken cancellationToken); 44 | 45 | /// 46 | /// A handler for requests that return responses. 47 | /// 48 | /// 49 | /// The request message type. 50 | /// 51 | /// 52 | /// The response message type. 53 | /// 54 | /// 55 | /// The request message. 56 | /// 57 | /// 58 | /// A that can be used to cancel the operation. 59 | /// 60 | /// 61 | /// A representing the operation that resolves to the response message. 62 | /// 63 | public delegate Task RequestHandler(TRequest request, CancellationToken cancellationToken); 64 | 65 | /// 66 | /// A handler for log messages sent from the language server to the client. 67 | /// 68 | /// 69 | /// The log message. 70 | /// 71 | /// 72 | /// The log message type. 73 | /// 74 | public delegate void LogMessageHandler(string message, MessageType messageType); 75 | 76 | /// 77 | /// A handler for diagnostics published by the language server. 78 | /// 79 | /// 80 | /// The URI of the document that the diagnostics apply to. 81 | /// 82 | /// 83 | /// A list of s. 84 | /// 85 | /// 86 | /// The diagnostics should replace any previously published diagnostics for the specified document. 87 | /// 88 | public delegate void PublishDiagnosticsHandler(Uri documentUri, List diagnostics); 89 | } 90 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/DelegateEmptyNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace LSP.Client.Handlers 5 | { 6 | /// 7 | /// A delegate-based handler for empty notifications. 8 | /// 9 | public class DelegateEmptyNotificationHandler 10 | : DelegateHandler, IInvokeEmptyNotificationHandler 11 | { 12 | /// 13 | /// Create a new . 14 | /// 15 | /// 16 | /// The name of the method handled by the handler. 17 | /// 18 | /// 19 | /// The delegate that implements the handler. 20 | /// 21 | public DelegateEmptyNotificationHandler(string method, EmptyNotificationHandler handler) 22 | : base(method) 23 | { 24 | if (handler == null) 25 | throw new ArgumentNullException(nameof(handler)); 26 | 27 | Handler = handler; 28 | } 29 | 30 | /// 31 | /// The delegate that implements the handler. 32 | /// 33 | public EmptyNotificationHandler Handler { get; } 34 | 35 | /// 36 | /// The kind of handler. 37 | /// 38 | public override HandlerKind Kind => HandlerKind.EmptyNotification; 39 | 40 | /// 41 | /// Invoke the handler. 42 | /// 43 | /// 44 | /// A representing the operation. 45 | /// 46 | public async Task Invoke() 47 | { 48 | await Task.Yield(); 49 | 50 | Handler(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/DelegateHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LSP.Client.Handlers 4 | { 5 | /// 6 | /// The base class for delegate-based message handlers. 7 | /// 8 | public abstract class DelegateHandler 9 | : IHandler 10 | { 11 | /// 12 | /// Create a new . 13 | /// 14 | /// 15 | /// The name of the method handled by the handler. 16 | /// 17 | protected DelegateHandler(string method) 18 | { 19 | if (String.IsNullOrWhiteSpace(method)) 20 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 21 | 22 | Method = method; 23 | } 24 | 25 | /// 26 | /// The name of the method handled by the handler. 27 | /// 28 | public string Method { get; } 29 | 30 | /// 31 | /// The kind of handler. 32 | /// 33 | public abstract HandlerKind Kind { get; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/DelegateNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace LSP.Client.Handlers 6 | { 7 | /// 8 | /// A delegate-based handler for notifications. 9 | /// 10 | /// 11 | /// The notification message type. 12 | /// 13 | public class DelegateNotificationHandler 14 | : DelegateHandler, IInvokeNotificationHandler 15 | { 16 | /// 17 | /// Create a new . 18 | /// 19 | /// 20 | /// The name of the method handled by the handler. 21 | /// 22 | /// 23 | /// The delegate that implements the handler. 24 | /// 25 | public DelegateNotificationHandler(string method, NotificationHandler handler) 26 | : base(method) 27 | { 28 | if (handler == null) 29 | throw new ArgumentNullException(nameof(handler)); 30 | 31 | Handler = handler; 32 | } 33 | 34 | /// 35 | /// The delegate that implements the handler. 36 | /// 37 | public NotificationHandler Handler { get; } 38 | 39 | /// 40 | /// The kind of handler. 41 | /// 42 | public override HandlerKind Kind => HandlerKind.EmptyNotification; 43 | 44 | /// 45 | /// Invoke the handler. 46 | /// 47 | /// 48 | /// The notification message. 49 | /// 50 | /// 51 | /// A representing the operation. 52 | /// 53 | public async Task Invoke(JObject notification) 54 | { 55 | await Task.Yield(); 56 | 57 | Handler( 58 | notification != null ? notification.ToObject() : default(TNotification) 59 | ); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/DelegateRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace LSP.Client.Handlers 7 | { 8 | /// 9 | /// A delegate-based handler for requests whose responses have no payload (i.e. void return type). 10 | /// 11 | /// 12 | /// The request message type. 13 | /// 14 | public class DelegateRequestHandler 15 | : DelegateHandler, IInvokeRequestHandler 16 | { 17 | /// 18 | /// Create a new . 19 | /// 20 | /// 21 | /// The name of the method handled by the handler. 22 | /// 23 | /// 24 | /// The delegate that implements the handler. 25 | /// 26 | public DelegateRequestHandler(string method, RequestHandler handler) 27 | : base(method) 28 | { 29 | if (handler == null) 30 | throw new ArgumentNullException(nameof(handler)); 31 | 32 | Handler = handler; 33 | } 34 | 35 | /// 36 | /// The delegate that implements the handler. 37 | /// 38 | public RequestHandler Handler { get; } 39 | 40 | /// 41 | /// The kind of handler. 42 | /// 43 | public override HandlerKind Kind => HandlerKind.Request; 44 | 45 | /// 46 | /// Invoke the handler. 47 | /// 48 | /// 49 | /// The request message. 50 | /// 51 | /// 52 | /// A that can be used to cancel the operation. 53 | /// 54 | /// 55 | /// A representing the operation. 56 | /// 57 | public async Task Invoke(JObject request, CancellationToken cancellationToken) 58 | { 59 | await Handler( 60 | request != null ? request.ToObject() : default(TRequest), 61 | cancellationToken 62 | ); 63 | 64 | return null; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/DelegateRequestResponseHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace LSP.Client.Handlers 7 | { 8 | /// 9 | /// A delegate-based handler for requests whose responses have payloads. 10 | /// 11 | /// 12 | /// The request message type. 13 | /// 14 | /// 15 | /// The response message type. 16 | /// 17 | public class DelegateRequestResponseHandler 18 | : DelegateHandler, IInvokeRequestHandler 19 | { 20 | /// 21 | /// Create a new . 22 | /// 23 | /// 24 | /// The name of the method handled by the handler. 25 | /// 26 | /// 27 | /// The delegate that implements the handler. 28 | /// 29 | public DelegateRequestResponseHandler(string method, RequestHandler handler) 30 | : base(method) 31 | { 32 | if (handler == null) 33 | throw new ArgumentNullException(nameof(handler)); 34 | 35 | Handler = handler; 36 | } 37 | 38 | /// 39 | /// The delegate that implements the handler. 40 | /// 41 | public RequestHandler Handler { get; } 42 | 43 | /// 44 | /// The kind of handler. 45 | /// 46 | public override HandlerKind Kind => HandlerKind.Request; 47 | 48 | /// 49 | /// Invoke the handler. 50 | /// 51 | /// 52 | /// The request message. 53 | /// 54 | /// 55 | /// A that can be used to cancel the operation. 56 | /// 57 | /// 58 | /// A representing the operation. 59 | /// 60 | public async Task Invoke(JObject request, CancellationToken cancellationToken) 61 | { 62 | return await Handler( 63 | request != null ? request.ToObject() : default(TRequest), 64 | cancellationToken 65 | ); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/DynamicRegistrationHandler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.LanguageServer.Capabilities.Server; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSP.Client.Handlers 8 | { 9 | /// 10 | /// Handler for "client/registerCapability". 11 | /// 12 | /// 13 | /// For now, this handler does nothing other than a void reply; we don't support dynamic registrations yet. 14 | /// 15 | public class DynamicRegistrationHandler 16 | : IInvokeRequestHandler 17 | { 18 | /// 19 | /// Create a new . 20 | /// 21 | public DynamicRegistrationHandler() 22 | { 23 | } 24 | 25 | /// 26 | /// Server capabilities dynamically updated by the handler. 27 | /// 28 | public ServerCapabilities ServerCapabilities { get; set; } = new ServerCapabilities(); 29 | 30 | /// 31 | /// The name of the method handled by the handler. 32 | /// 33 | public string Method => "client/registerCapability"; 34 | 35 | /// 36 | /// The kind of handler. 37 | /// 38 | public HandlerKind Kind => HandlerKind.Request; 39 | 40 | /// 41 | /// Invoke the handler. 42 | /// 43 | /// 44 | /// The request message. 45 | /// 46 | /// 47 | /// A that can be used to cancel the operation. 48 | /// 49 | /// 50 | /// A representing the operation. 51 | /// 52 | public Task Invoke(JObject request, CancellationToken cancellationToken) 53 | { 54 | // For now, we don't really support dynamic registration but OmniSharp's implementation sends a request even when dynamic registrations are not supported. 55 | 56 | return Task.FromResult(null); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/HandlerKind.cs: -------------------------------------------------------------------------------- 1 | namespace LSP.Client.Handlers 2 | { 3 | /// 4 | /// Represents a well-known kind of message handler. 5 | /// 6 | public enum HandlerKind 7 | { 8 | /// 9 | /// A handler for empty notifications. 10 | /// 11 | EmptyNotification, 12 | 13 | /// 14 | /// A handler for notifications. 15 | /// 16 | Notification, 17 | 18 | /// 19 | /// A handler for requests. 20 | /// 21 | Request 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/IHandler.cs: -------------------------------------------------------------------------------- 1 | namespace LSP.Client.Handlers 2 | { 3 | /// 4 | /// Represents a client-side message handler. 5 | /// 6 | public interface IHandler 7 | { 8 | /// 9 | /// The name of the method handled by the handler. 10 | /// 11 | string Method { get; } 12 | 13 | /// 14 | /// The kind of handler. 15 | /// 16 | HandlerKind Kind { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/IInvokeEmptyNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace LSP.Client.Handlers 4 | { 5 | /// 6 | /// Represents a handler for empty notifications. 7 | /// 8 | public interface IInvokeEmptyNotificationHandler 9 | : IHandler 10 | { 11 | /// 12 | /// Invoke the handler. 13 | /// 14 | /// 15 | /// A representing the operation. 16 | /// 17 | Task Invoke(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/IInvokeNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Threading.Tasks; 3 | 4 | namespace LSP.Client.Handlers 5 | { 6 | /// 7 | /// Represents a handler for notifications. 8 | /// 9 | public interface IInvokeNotificationHandler 10 | : IHandler 11 | { 12 | /// 13 | /// Invoke the handler. 14 | /// 15 | /// 16 | /// The notification message. 17 | /// 18 | /// 19 | /// A representing the operation. 20 | /// 21 | Task Invoke(JObject notification); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/IInvokeRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace LSP.Client.Handlers 6 | { 7 | /// 8 | /// Represents a handler for requests. 9 | /// 10 | public interface IInvokeRequestHandler 11 | : IHandler 12 | { 13 | /// 14 | /// Invoke the handler. 15 | /// 16 | /// 17 | /// The request message. 18 | /// 19 | /// 20 | /// A that can be used to cancel the operation. 21 | /// 22 | /// 23 | /// A representing the operation. 24 | /// 25 | Task Invoke(JObject request, CancellationToken cancellationToken); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/JsonRpcEmptyNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace LSP.Client.Handlers 6 | { 7 | /// 8 | /// An empty notification handler that invokes a JSON-RPC . 9 | /// 10 | public class JsonRpcEmptyNotificationHandler 11 | : JsonRpcHandler, IInvokeEmptyNotificationHandler 12 | { 13 | /// 14 | /// Create a new . 15 | /// 16 | /// 17 | /// The name of the method handled by the handler. 18 | /// 19 | /// 20 | /// The underlying JSON-RPC . 21 | /// 22 | public JsonRpcEmptyNotificationHandler(string method, INotificationHandler handler) 23 | : base(method) 24 | { 25 | if (handler == null) 26 | throw new ArgumentNullException(nameof(handler)); 27 | 28 | Handler = handler; 29 | } 30 | 31 | /// 32 | /// The underlying JSON-RPC . 33 | /// 34 | public INotificationHandler Handler { get; } 35 | 36 | /// 37 | /// The kind of handler. 38 | /// 39 | public override HandlerKind Kind => HandlerKind.EmptyNotification; 40 | 41 | /// 42 | /// Invoke the handler. 43 | /// 44 | /// 45 | /// A representing the operation. 46 | /// 47 | public Task Invoke() => Handler.Handle(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/JsonRpcHandler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using System; 3 | 4 | namespace LSP.Client.Handlers 5 | { 6 | /// 7 | /// The base class for message handlers based on JSON-RPC s. 8 | /// 9 | public abstract class JsonRpcHandler 10 | : IHandler 11 | { 12 | /// 13 | /// Create a new . 14 | /// 15 | /// 16 | /// The name of the method handled by the handler. 17 | /// 18 | protected JsonRpcHandler(string method) 19 | { 20 | if (String.IsNullOrWhiteSpace(method)) 21 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 22 | 23 | Method = method; 24 | } 25 | 26 | /// 27 | /// The name of the method handled by the handler. 28 | /// 29 | public string Method { get; } 30 | 31 | /// 32 | /// The kind of handler. 33 | /// 34 | public abstract HandlerKind Kind { get; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/LSP.Client/Handlers/JsonRpcNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | namespace LSP.Client.Handlers 7 | { 8 | /// 9 | /// A notification handler that invokes a JSON-RPC . 10 | /// 11 | /// 12 | /// The notification message handler. 13 | /// 14 | public class JsonRpcNotificationHandler 15 | : JsonRpcHandler, IInvokeNotificationHandler 16 | { 17 | /// 18 | /// Create a new . 19 | /// 20 | /// 21 | /// The name of the method handled by the handler. 22 | /// 23 | /// 24 | /// The underlying JSON-RPC . 25 | /// 26 | public JsonRpcNotificationHandler(string method, INotificationHandler handler) 27 | : base(method) 28 | { 29 | if (handler == null) 30 | throw new ArgumentNullException(nameof(handler)); 31 | 32 | Handler = handler; 33 | } 34 | 35 | /// 36 | /// The underlying JSON-RPC . 37 | /// 38 | public INotificationHandler Handler { get; } 39 | 40 | /// 41 | /// The kind of handler. 42 | /// 43 | public override HandlerKind Kind => HandlerKind.Notification; 44 | 45 | /// 46 | /// Invoke the handler. 47 | /// 48 | /// 49 | /// A representing the notification parameters. 50 | /// 51 | /// 52 | /// A representing the operation. 53 | /// 54 | public Task Invoke(JObject notification) => Handler.Handle( 55 | notification != null ? notification.ToObject() : default(TNotification) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/LSP.Client/LSP.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/LSP.Client/LanguageClientRegistration.cs: -------------------------------------------------------------------------------- 1 | using OmniSharp.Extensions.JsonRpc; 2 | using System; 3 | 4 | namespace LSP.Client 5 | { 6 | using Handlers; 7 | 8 | /// 9 | /// Extension methods for enabling various styles of handler registration. 10 | /// 11 | public static class LanguageRegistration 12 | { 13 | /// 14 | /// Register a handler for empty notifications. 15 | /// 16 | /// 17 | /// The . 18 | /// 19 | /// 20 | /// The name of the notification method to handle. 21 | /// 22 | /// 23 | /// A delegate that implements the handler. 24 | /// 25 | /// 26 | /// An representing the registration. 27 | /// 28 | public static IDisposable HandleEmptyNotification(this LanguageClient languageClient, string method, EmptyNotificationHandler handler) 29 | { 30 | if (languageClient == null) 31 | throw new ArgumentNullException(nameof(languageClient)); 32 | 33 | if (String.IsNullOrWhiteSpace(method)) 34 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 35 | 36 | if (handler == null) 37 | throw new ArgumentNullException(nameof(handler)); 38 | 39 | return languageClient.RegisterHandler( 40 | new DelegateEmptyNotificationHandler(method, handler) 41 | ); 42 | } 43 | 44 | /// 45 | /// Register a handler for empty notifications. 46 | /// 47 | /// 48 | /// The . 49 | /// 50 | /// 51 | /// The name of the notification method to handle. 52 | /// 53 | /// 54 | /// A JSON-RPC that implements the handler. 55 | /// 56 | /// 57 | /// An representing the registration. 58 | /// 59 | public static IDisposable HandleEmptyNotification(this LanguageClient languageClient, string method, INotificationHandler handler) 60 | { 61 | if (languageClient == null) 62 | throw new ArgumentNullException(nameof(languageClient)); 63 | 64 | if (String.IsNullOrWhiteSpace(method)) 65 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 66 | 67 | if (handler == null) 68 | throw new ArgumentNullException(nameof(handler)); 69 | 70 | return languageClient.RegisterHandler( 71 | new JsonRpcEmptyNotificationHandler(method, handler) 72 | ); 73 | } 74 | 75 | 76 | /// 77 | /// Register a handler for notifications. 78 | /// 79 | /// 80 | /// The notification message type. 81 | /// 82 | /// 83 | /// The . 84 | /// 85 | /// 86 | /// The name of the notification method to handle. 87 | /// 88 | /// 89 | /// A delegate that implements the handler. 90 | /// 91 | /// 92 | /// An representing the registration. 93 | /// 94 | public static IDisposable HandleNotification(this LanguageClient languageClient, string method, NotificationHandler handler) 95 | { 96 | if (languageClient == null) 97 | throw new ArgumentNullException(nameof(languageClient)); 98 | 99 | if (String.IsNullOrWhiteSpace(method)) 100 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 101 | 102 | if (handler == null) 103 | throw new ArgumentNullException(nameof(handler)); 104 | 105 | return languageClient.RegisterHandler( 106 | new DelegateNotificationHandler(method, handler) 107 | ); 108 | } 109 | 110 | /// 111 | /// Register a handler for notifications. 112 | /// 113 | /// 114 | /// The notification message type. 115 | /// 116 | /// 117 | /// The . 118 | /// 119 | /// 120 | /// The name of the notification method to handle. 121 | /// 122 | /// 123 | /// A JSON-RPC that implements the handler. 124 | /// 125 | /// 126 | /// An representing the registration. 127 | /// 128 | public static IDisposable HandleNotification(this LanguageClient languageClient, string method, INotificationHandler handler) 129 | { 130 | if (languageClient == null) 131 | throw new ArgumentNullException(nameof(languageClient)); 132 | 133 | if (String.IsNullOrWhiteSpace(method)) 134 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 135 | 136 | if (handler == null) 137 | throw new ArgumentNullException(nameof(handler)); 138 | 139 | return languageClient.RegisterHandler( 140 | new JsonRpcNotificationHandler(method, handler) 141 | ); 142 | } 143 | 144 | /// 145 | /// Register a handler for requests. 146 | /// 147 | /// 148 | /// The request message type. 149 | /// 150 | /// 151 | /// The . 152 | /// 153 | /// 154 | /// The name of the request method to handle. 155 | /// 156 | /// 157 | /// A delegate that implements the handler. 158 | /// 159 | /// 160 | /// An representing the registration. 161 | /// 162 | public static IDisposable HandleRequest(this LanguageClient languageClient, string method, RequestHandler handler) 163 | { 164 | if (languageClient == null) 165 | throw new ArgumentNullException(nameof(languageClient)); 166 | 167 | if (String.IsNullOrWhiteSpace(method)) 168 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 169 | 170 | if (handler == null) 171 | throw new ArgumentNullException(nameof(handler)); 172 | 173 | return languageClient.RegisterHandler( 174 | new DelegateRequestHandler(method, handler) 175 | ); 176 | } 177 | 178 | /// 179 | /// Register a handler for requests. 180 | /// 181 | /// 182 | /// The request message type. 183 | /// 184 | /// 185 | /// The response message type. 186 | /// 187 | /// 188 | /// The . 189 | /// 190 | /// 191 | /// The name of the request method to handle. 192 | /// 193 | /// 194 | /// A delegate that implements the handler. 195 | /// 196 | /// 197 | /// An representing the registration. 198 | /// 199 | public static IDisposable HandleRequest(this LanguageClient languageClient, string method, RequestHandler handler) 200 | { 201 | if (languageClient == null) 202 | throw new ArgumentNullException(nameof(languageClient)); 203 | 204 | if (String.IsNullOrWhiteSpace(method)) 205 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 206 | 207 | if (handler == null) 208 | throw new ArgumentNullException(nameof(handler)); 209 | 210 | return languageClient.RegisterHandler( 211 | new DelegateRequestResponseHandler(method, handler) 212 | ); 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/LSP.Client/Logging/OverwriteSourceContextEnricher.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Core; 2 | using System; 3 | using Serilog.Events; 4 | 5 | namespace LSP.Client.Logging 6 | { 7 | /// 8 | /// Serilog event enricher that always sets the "SourceContext" property in log messages (even if it is already present). 9 | /// 10 | class OverwriteSourceContextEnricher 11 | : ILogEventEnricher 12 | { 13 | /// 14 | /// The "SourceContext" value to use. 15 | /// 16 | readonly string _sourceContext; 17 | 18 | /// 19 | /// Create a new . 20 | /// 21 | /// 22 | /// The "SourceContext" value to use. 23 | /// 24 | public OverwriteSourceContextEnricher(string sourceContext) 25 | { 26 | if (sourceContext == null) 27 | throw new ArgumentNullException(nameof(sourceContext)); 28 | 29 | _sourceContext = sourceContext; 30 | } 31 | 32 | /// 33 | /// Enrich the specified log event. 34 | /// 35 | /// 36 | /// The target log event. 37 | /// 38 | /// 39 | /// A log event property factory used to create the "SourceContext" property. 40 | /// 41 | public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) 42 | { 43 | if (logEvent == null) 44 | throw new ArgumentNullException(nameof(logEvent)); 45 | 46 | if (propertyFactory == null) 47 | throw new ArgumentNullException(nameof(propertyFactory)); 48 | 49 | LogEventProperty sourceContextProperty = propertyFactory.CreateProperty("SourceContext", _sourceContext); 50 | logEvent.AddOrUpdateProperty(sourceContextProperty); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/LSP.Client/Logging/SerilogExtensions.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | 4 | namespace LSP.Client.Logging 5 | { 6 | /// 7 | /// Extension methods for working with Serilog. 8 | /// 9 | public static class SerilogExtensions 10 | { 11 | /// 12 | /// Create a new logger that uses the specified type for "SourceContext". 13 | /// 14 | /// 15 | /// The source type. 16 | /// 17 | /// 18 | /// The base . 19 | /// 20 | /// 21 | /// The new . 22 | /// 23 | /// 24 | /// Unlike , this will overwrite the "SourceContext" property if it has already been set for the base logger. 25 | /// 26 | public static ILogger ForSourceContext(this ILogger logger) 27 | { 28 | if (logger == null) 29 | throw new ArgumentNullException(nameof(logger)); 30 | 31 | return logger.ForContext(typeof(TSource)); 32 | } 33 | 34 | /// 35 | /// Create a new logger that uses the full name of the specified type for "SourceContext". 36 | /// 37 | /// 38 | /// The base . 39 | /// 40 | /// 41 | /// The source type. 42 | /// 43 | /// 44 | /// The new . 45 | /// 46 | /// 47 | /// Unlike , this will overwrite the "SourceContext" property if it has already been set for the base logger. 48 | /// 49 | public static ILogger ForSourceContext(this ILogger logger, Type source) 50 | { 51 | if (logger == null) 52 | throw new ArgumentNullException(nameof(logger)); 53 | 54 | if (source == null) 55 | throw new ArgumentNullException(nameof(source)); 56 | 57 | return logger.ForSourceContext(source.FullName); 58 | } 59 | 60 | /// 61 | /// Create a new logger that uses the specified value for "SourceContext". 62 | /// 63 | /// 64 | /// The base . 65 | /// 66 | /// 67 | /// The source context value. 68 | /// 69 | /// 70 | /// The new . 71 | /// 72 | /// 73 | /// Unlike , this will overwrite the "SourceContext" property if it has already been set for the base logger. 74 | /// 75 | public static ILogger ForSourceContext(this ILogger logger, string sourceContext) 76 | { 77 | if (logger == null) 78 | throw new ArgumentNullException(nameof(logger)); 79 | 80 | if (sourceContext == null) 81 | throw new ArgumentNullException(nameof(sourceContext)); 82 | 83 | return logger.ForContext( 84 | new OverwriteSourceContextEnricher(sourceContext) 85 | ); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/LSP.Client/LspErrorCodes.cs: -------------------------------------------------------------------------------- 1 | namespace LSP.Client 2 | { 3 | /// 4 | /// Well-known LSP error codes. 5 | /// 6 | public static class LspErrorCodes 7 | { 8 | /// 9 | /// No error code was supplied. 10 | /// 11 | public static readonly int None = -32001; 12 | 13 | /// 14 | /// Server has not been initialised. 15 | /// 16 | public const int ServerNotInitialized = -32002; 17 | 18 | /// 19 | /// Method not found. 20 | /// 21 | public const int MethodNotSupported = -32601; 22 | 23 | /// 24 | /// Invalid request. 25 | /// 26 | public const int InvalidRequest = -32600; 27 | 28 | /// 29 | /// Invalid request parameters. 30 | /// 31 | public const int InvalidParameters = -32602; 32 | 33 | /// 34 | /// Internal error. 35 | /// 36 | public const int InternalError = -32603; 37 | 38 | /// 39 | /// Unable to parse request. 40 | /// 41 | public const int ParseError = -32700; 42 | 43 | /// 44 | /// Request was cancelled. 45 | /// 46 | public const int RequestCancelled = -32800; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LSP.Client/Processes/NamedPipeServerProcess.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Pipes; 5 | using System.Threading.Tasks; 6 | 7 | namespace LSP.Client.Processes 8 | { 9 | /// 10 | /// A is a that creates named pipe streams to connect a language client to a language server in the same process. 11 | /// 12 | public class NamedPipeServerProcess 13 | : ServerProcess 14 | { 15 | /// 16 | /// Create a new . 17 | /// 18 | /// 19 | /// The base name (prefix) used to create the named pipes. 20 | /// 21 | /// 22 | /// The logger to use. 23 | /// 24 | public NamedPipeServerProcess(string baseName, ILogger logger) 25 | : base(logger) 26 | { 27 | BaseName = baseName; 28 | } 29 | 30 | /// 31 | /// Dispose of resources being used by the . 32 | /// 33 | /// 34 | /// Explicit disposal? 35 | /// 36 | protected override void Dispose(bool disposing) 37 | { 38 | if (disposing) 39 | CloseStreams(); 40 | 41 | base.Dispose(disposing); 42 | } 43 | 44 | /// 45 | /// The base name (prefix) used to create the named pipes. 46 | /// 47 | public string BaseName { get; } 48 | 49 | /// 50 | /// Is the server running? 51 | /// 52 | public override bool IsRunning => ServerStartCompletion.Task.IsCompleted; 53 | 54 | /// 55 | /// A that the client reads messages from. 56 | /// 57 | public NamedPipeClientStream ClientInputStream { get; protected set; } 58 | 59 | /// 60 | /// A that the client writes messages to. 61 | /// 62 | public NamedPipeClientStream ClientOutputStream { get; protected set; } 63 | 64 | /// 65 | /// A that the server reads messages from. 66 | /// 67 | public NamedPipeServerStream ServerInputStream { get; protected set; } 68 | 69 | /// 70 | /// A that the server writes messages to. 71 | /// 72 | public NamedPipeServerStream ServerOutputStream { get; protected set; } 73 | 74 | /// 75 | /// The server's input stream. 76 | /// 77 | public override Stream InputStream => ServerInputStream; 78 | 79 | /// 80 | /// The server's output stream. 81 | /// 82 | public override Stream OutputStream => ServerOutputStream; 83 | 84 | /// 85 | /// Start or connect to the server. 86 | /// 87 | /// 88 | /// A representing the operation. 89 | /// 90 | public override async Task Start() 91 | { 92 | ServerExitCompletion = new TaskCompletionSource(); 93 | 94 | ServerInputStream = new NamedPipeServerStream(BaseName + "/in", PipeDirection.Out, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: 1024, outBufferSize: 1024); 95 | ServerOutputStream = new NamedPipeServerStream(BaseName + "/out", PipeDirection.In, 2, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, inBufferSize: 1024, outBufferSize: 1024); 96 | ClientInputStream = new NamedPipeClientStream(".", BaseName + "/out", PipeDirection.Out, PipeOptions.Asynchronous); 97 | ClientOutputStream = new NamedPipeClientStream(".", BaseName + "/in", PipeDirection.In, PipeOptions.Asynchronous); 98 | 99 | // Ensure all pipes are connected before proceeding. 100 | await Task.WhenAll( 101 | ServerInputStream.WaitForConnectionAsync(), 102 | ServerOutputStream.WaitForConnectionAsync(), 103 | ClientInputStream.ConnectAsync(), 104 | ClientOutputStream.ConnectAsync() 105 | ); 106 | 107 | ServerStartCompletion.TrySetResult(null); 108 | } 109 | 110 | /// 111 | /// Stop the server. 112 | /// 113 | /// 114 | /// A representing the operation. 115 | /// 116 | public override Task Stop() 117 | { 118 | ServerStartCompletion = new TaskCompletionSource(); 119 | 120 | CloseStreams(); 121 | 122 | ServerExitCompletion.TrySetResult(null); 123 | 124 | return Task.CompletedTask; 125 | } 126 | 127 | /// 128 | /// Close the underlying streams. 129 | /// 130 | void CloseStreams() 131 | { 132 | ClientInputStream?.Dispose(); 133 | ClientInputStream = null; 134 | 135 | ClientOutputStream?.Dispose(); 136 | ClientOutputStream = null; 137 | 138 | ServerInputStream?.Dispose(); 139 | ServerInputStream = null; 140 | 141 | ServerOutputStream?.Dispose(); 142 | ServerOutputStream = null; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/LSP.Client/Processes/ServerProcess.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | 6 | namespace LSP.Client.Processes 7 | { 8 | using Logging; 9 | 10 | /// 11 | /// A is responsible for launching or attaching to a language server, providing access to its input and output streams, and tracking its lifetime. 12 | /// 13 | public abstract class ServerProcess 14 | : IDisposable 15 | { 16 | /// 17 | /// Create a new . 18 | /// 19 | /// 20 | /// The logger to use. 21 | /// 22 | protected ServerProcess(ILogger logger) 23 | { 24 | if (logger == null) 25 | throw new ArgumentNullException(nameof(logger)); 26 | 27 | Log = logger.ForSourceContext( 28 | source: GetType() 29 | ); 30 | 31 | ServerStartCompletion = new TaskCompletionSource(); 32 | 33 | ServerExitCompletion = new TaskCompletionSource(); 34 | ServerExitCompletion.SetResult(null); // Start out as if the server has already exited. 35 | } 36 | 37 | /// 38 | /// Finaliser for . 39 | /// 40 | ~ServerProcess() 41 | { 42 | Dispose(false); 43 | } 44 | 45 | /// 46 | /// Dispose of resources being used by the launcher. 47 | /// 48 | public void Dispose() 49 | { 50 | Dispose(true); 51 | } 52 | 53 | /// 54 | /// Dispose of resources being used by the launcher. 55 | /// 56 | /// 57 | /// Explicit disposal? 58 | /// 59 | protected virtual void Dispose(bool disposing) 60 | { 61 | } 62 | 63 | /// 64 | /// The launcher's logger. 65 | /// 66 | protected ILogger Log { get; } 67 | 68 | /// 69 | /// The used to signal server startup. 70 | /// 71 | protected TaskCompletionSource ServerStartCompletion { get; set; } 72 | 73 | /// 74 | /// The used to signal server exit. 75 | /// 76 | protected TaskCompletionSource ServerExitCompletion { get; set; } 77 | 78 | /// 79 | /// Event raised when the server has exited. 80 | /// 81 | public event EventHandler Exited; 82 | 83 | /// 84 | /// Is the server running? 85 | /// 86 | public abstract bool IsRunning { get; } 87 | 88 | /// 89 | /// A that completes when the server has started. 90 | /// 91 | public Task HasStarted => ServerStartCompletion.Task; 92 | 93 | /// 94 | /// A that completes when the server has exited. 95 | /// 96 | public Task HasExited => ServerExitCompletion.Task; 97 | 98 | /// 99 | /// The server's input stream. 100 | /// 101 | /// 102 | /// The connection will write to the server's input stream, and read from its output stream. 103 | /// 104 | public abstract Stream InputStream { get; } 105 | 106 | /// 107 | /// The server's output stream. 108 | /// 109 | /// 110 | /// The connection will read from the server's output stream, and write to its input stream. 111 | /// 112 | public abstract Stream OutputStream { get; } 113 | 114 | /// 115 | /// Start or connect to the server. 116 | /// 117 | public abstract Task Start(); 118 | 119 | /// 120 | /// Stop or disconnect from the server. 121 | /// 122 | public abstract Task Stop(); 123 | 124 | /// 125 | /// Raise the event. 126 | /// 127 | protected virtual void OnExited() 128 | { 129 | Exited?.Invoke(this, EventArgs.Empty); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/LSP.Client/Processes/StdioServerProcess.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace LSP.Client.Processes 9 | { 10 | /// 11 | /// A is a that launches its server as an external process and communicates with it over STDIN / STDOUT. 12 | /// 13 | public class StdioServerProcess 14 | : ServerProcess 15 | { 16 | /// 17 | /// A that describes how to start the server. 18 | /// 19 | readonly ProcessStartInfo _serverStartInfo; 20 | 21 | /// 22 | /// The current server process (if any). 23 | /// 24 | Process _serverProcess; 25 | 26 | /// 27 | /// Create a new . 28 | /// 29 | /// 30 | /// The logger to use. 31 | /// 32 | /// 33 | /// A that describes how to start the server. 34 | /// 35 | public StdioServerProcess(ILogger logger, ProcessStartInfo serverStartInfo) 36 | : base(logger) 37 | { 38 | if (serverStartInfo == null) 39 | throw new ArgumentNullException(nameof(serverStartInfo)); 40 | 41 | _serverStartInfo = serverStartInfo; 42 | } 43 | 44 | /// 45 | /// Dispose of resources being used by the launcher. 46 | /// 47 | /// 48 | /// Explicit disposal? 49 | /// 50 | protected override void Dispose(bool disposing) 51 | { 52 | if (disposing) 53 | { 54 | Process serverProcess = Interlocked.Exchange(ref _serverProcess, null); 55 | if (serverProcess != null) 56 | { 57 | if (!serverProcess.HasExited) 58 | serverProcess.Kill(); 59 | 60 | serverProcess.Dispose(); 61 | } 62 | } 63 | } 64 | 65 | /// 66 | /// Is the server running? 67 | /// 68 | public override bool IsRunning => !ServerExitCompletion.Task.IsCompleted; 69 | 70 | /// 71 | /// The server's input stream. 72 | /// 73 | public override Stream InputStream => _serverProcess?.StandardInput?.BaseStream; 74 | 75 | /// 76 | /// The server's output stream. 77 | /// 78 | public override Stream OutputStream => _serverProcess?.StandardOutput?.BaseStream; 79 | 80 | /// 81 | /// Start or connect to the server. 82 | /// 83 | public override Task Start() 84 | { 85 | ServerExitCompletion = new TaskCompletionSource(); 86 | 87 | _serverStartInfo.CreateNoWindow = true; 88 | _serverStartInfo.UseShellExecute = false; 89 | _serverStartInfo.RedirectStandardInput = true; 90 | _serverStartInfo.RedirectStandardOutput = true; 91 | 92 | Process serverProcess = _serverProcess = new Process 93 | { 94 | StartInfo = _serverStartInfo, 95 | EnableRaisingEvents = true 96 | }; 97 | serverProcess.Exited += ServerProcess_Exit; 98 | 99 | if (!serverProcess.Start()) 100 | throw new InvalidOperationException("Failed to launch language server ."); 101 | 102 | ServerStartCompletion.TrySetResult(null); 103 | 104 | return Task.CompletedTask; 105 | } 106 | 107 | /// 108 | /// Stop or disconnect from the server. 109 | /// 110 | public override async Task Stop() 111 | { 112 | Process serverProcess = Interlocked.Exchange(ref _serverProcess, null); 113 | if (serverProcess != null && !serverProcess.HasExited) 114 | serverProcess.Kill(); 115 | 116 | await ServerExitCompletion.Task; 117 | } 118 | 119 | /// 120 | /// Called when the server process has exited. 121 | /// 122 | /// 123 | /// The event sender. 124 | /// 125 | /// 126 | /// The event arguments. 127 | /// 128 | void ServerProcess_Exit(object sender, EventArgs args) 129 | { 130 | Log.Verbose("Server process has exited."); 131 | 132 | OnExited(); 133 | ServerExitCompletion.TrySetResult(null); 134 | ServerStartCompletion = new TaskCompletionSource(); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/LSP.Client/Protocol/ClientMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Newtonsoft.Json.Serialization; 4 | 5 | namespace LSP.Client.Protocol 6 | { 7 | /// 8 | /// The client-side representation of an LSP message. 9 | /// 10 | [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] 11 | public class ClientMessage 12 | { 13 | /// 14 | /// The JSON-RPC protocol version. 15 | /// 16 | [JsonProperty("jsonrpc")] 17 | public string ProtocolVersion => "2.0"; 18 | 19 | /// 20 | /// The request / response Id, if the message represents a request or a response. 21 | /// 22 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, NullValueHandling = NullValueHandling.Ignore)] 23 | public object Id { get; set; } 24 | 25 | /// 26 | /// The JSON-RPC method name. 27 | /// 28 | public string Method { get; set; } 29 | 30 | /// 31 | /// The request / notification message, if the message represents a request or a notification. 32 | /// 33 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, NullValueHandling = NullValueHandling.Ignore)] 34 | public JObject Params { get; set; } 35 | 36 | /// 37 | /// The response message, if the message represents a response. 38 | /// 39 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, NullValueHandling = NullValueHandling.Ignore)] 40 | public JObject Result { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LSP.Client/Protocol/ErrorMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Newtonsoft.Json.Serialization; 4 | 5 | namespace LSP.Client.Protocol 6 | { 7 | /// 8 | /// A JSON-RPC error message. 9 | /// 10 | [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] 11 | public class ErrorMessage 12 | { 13 | /// 14 | /// The error code. 15 | /// 16 | public int Code { get; set; } 17 | 18 | /// 19 | /// The error message. 20 | /// 21 | public string Message { get; set; } 22 | 23 | /// 24 | /// Optional data associated with the message. 25 | /// 26 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 27 | public JToken Data { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/LSP.Client/Protocol/LspConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace LSP.Client.Protocol 4 | { 5 | using Handlers; 6 | 7 | /// 8 | /// Extension methods for enabling various styles of handler registration. 9 | /// 10 | public static class LspConnectionExtensions 11 | { 12 | /// 13 | /// Register a handler for empty notifications. 14 | /// 15 | /// 16 | /// The . 17 | /// 18 | /// 19 | /// The name of the notification method to handle. 20 | /// 21 | /// 22 | /// A delegate that implements the handler. 23 | /// 24 | /// 25 | /// An representing the registration. 26 | /// 27 | public static IDisposable HandleEmptyNotification(this LspConnection clientConnection, string method, EmptyNotificationHandler handler) 28 | { 29 | if (clientConnection == null) 30 | throw new ArgumentNullException(nameof(clientConnection)); 31 | 32 | if (String.IsNullOrWhiteSpace(method)) 33 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 34 | 35 | if (handler == null) 36 | throw new ArgumentNullException(nameof(handler)); 37 | 38 | return clientConnection.RegisterHandler( 39 | new DelegateEmptyNotificationHandler(method, handler) 40 | ); 41 | } 42 | 43 | /// 44 | /// Register a handler for notifications. 45 | /// 46 | /// 47 | /// The notification message type. 48 | /// 49 | /// 50 | /// The . 51 | /// 52 | /// 53 | /// The name of the notification method to handle. 54 | /// 55 | /// 56 | /// A delegate that implements the handler. 57 | /// 58 | /// 59 | /// An representing the registration. 60 | /// 61 | public static IDisposable HandleNotification(this LspConnection clientConnection, string method, NotificationHandler handler) 62 | { 63 | if (clientConnection == null) 64 | throw new ArgumentNullException(nameof(clientConnection)); 65 | 66 | if (String.IsNullOrWhiteSpace(method)) 67 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 68 | 69 | if (handler == null) 70 | throw new ArgumentNullException(nameof(handler)); 71 | 72 | return clientConnection.RegisterHandler( 73 | new DelegateNotificationHandler(method, handler) 74 | ); 75 | } 76 | 77 | /// 78 | /// Register a handler for requests. 79 | /// 80 | /// 81 | /// The request message type. 82 | /// 83 | /// 84 | /// The . 85 | /// 86 | /// 87 | /// The name of the request method to handle. 88 | /// 89 | /// 90 | /// A delegate that implements the handler. 91 | /// 92 | /// 93 | /// An representing the registration. 94 | /// 95 | public static IDisposable HandleRequest(this LspConnection clientConnection, string method, RequestHandler handler) 96 | { 97 | if (clientConnection == null) 98 | throw new ArgumentNullException(nameof(clientConnection)); 99 | 100 | if (String.IsNullOrWhiteSpace(method)) 101 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 102 | 103 | if (handler == null) 104 | throw new ArgumentNullException(nameof(handler)); 105 | 106 | return clientConnection.RegisterHandler( 107 | new DelegateRequestHandler(method, handler) 108 | ); 109 | } 110 | 111 | /// 112 | /// Register a handler for requests. 113 | /// 114 | /// 115 | /// The request message type. 116 | /// 117 | /// 118 | /// The response message type. 119 | /// 120 | /// 121 | /// The . 122 | /// 123 | /// 124 | /// The name of the request method to handle. 125 | /// 126 | /// 127 | /// A delegate that implements the handler. 128 | /// 129 | /// 130 | /// An representing the registration. 131 | /// 132 | public static IDisposable HandleRequest(this LspConnection clientConnection, string method, RequestHandler handler) 133 | { 134 | if (clientConnection == null) 135 | throw new ArgumentNullException(nameof(clientConnection)); 136 | 137 | if (String.IsNullOrWhiteSpace(method)) 138 | throw new ArgumentException($"Argument cannot be null, empty, or entirely composed of whitespace: {nameof(method)}.", nameof(method)); 139 | 140 | if (handler == null) 141 | throw new ArgumentNullException(nameof(handler)); 142 | 143 | return clientConnection.RegisterHandler( 144 | new DelegateRequestResponseHandler(method, handler) 145 | ); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/LSP.Client/Protocol/ServerMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using Newtonsoft.Json.Serialization; 4 | 5 | namespace LSP.Client.Protocol 6 | { 7 | /// 8 | /// The server-side representation of an LSP message. 9 | /// 10 | [JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] 11 | public class ServerMessage 12 | { 13 | /// 14 | /// The JSON-RPC protocol version. 15 | /// 16 | public string ProtocolVersion { get; set; } = "2.0"; 17 | 18 | /// 19 | /// The request / response Id, if the message represents a request or a response. 20 | /// 21 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 22 | public object Id { get; set; } 23 | 24 | /// 25 | /// The JSON-RPC method name. 26 | /// 27 | public string Method { get; set; } 28 | 29 | /// 30 | /// The request / notification message, if the message represents a request or a notification. 31 | /// 32 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 33 | public JObject Params { get; set; } 34 | 35 | /// 36 | /// The response message, if the message represents a response. 37 | /// 38 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 39 | public JObject Result { get; set; } 40 | 41 | /// 42 | /// The response error (if any). 43 | /// 44 | [JsonProperty("error", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] 45 | public ErrorMessage Error { get; set; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/LSP.Client/Utilities/DocumentUri.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace LSP.Client.Utilities 5 | { 6 | /// 7 | /// Helper methods for working with LSP document URIs. 8 | /// 9 | public static class DocumentUri 10 | { 11 | /// 12 | /// Get the local file-system path for the specified document URI. 13 | /// 14 | /// 15 | /// The LSP document URI. 16 | /// 17 | /// 18 | /// The file-system path, or null if the URI does not represent a file-system path. 19 | /// 20 | public static string GetFileSystemPath(Uri documentUri) 21 | { 22 | if (documentUri == null) 23 | throw new ArgumentNullException(nameof(documentUri)); 24 | 25 | if (documentUri.Scheme != "file") 26 | return null; 27 | 28 | // The language server protocol represents "C:\Foo\Bar" as "file:///c:/foo/bar". 29 | string fileSystemPath = Uri.UnescapeDataString(documentUri.AbsolutePath); 30 | if (Path.DirectorySeparatorChar == '\\') 31 | { 32 | if (fileSystemPath.StartsWith("/")) 33 | fileSystemPath = fileSystemPath.Substring(1); 34 | 35 | fileSystemPath = fileSystemPath.Replace('/', '\\'); 36 | } 37 | 38 | return fileSystemPath; 39 | } 40 | 41 | /// 42 | /// Convert a file-system path to an LSP document URI. 43 | /// 44 | /// 45 | /// The file-system path. 46 | /// 47 | /// 48 | /// The LSP document URI. 49 | /// 50 | public static Uri FromFileSystemPath(string fileSystemPath) 51 | { 52 | if (String.IsNullOrWhiteSpace(fileSystemPath)) 53 | throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'fileSystemPath'.", nameof(fileSystemPath)); 54 | 55 | if (!Path.IsPathRooted(fileSystemPath)) 56 | throw new ArgumentException($"Path '{fileSystemPath}' is not an absolute path.", nameof(fileSystemPath)); 57 | 58 | if (Path.DirectorySeparatorChar == '\\') 59 | return new Uri("file:///" + fileSystemPath.Replace('\\', '/')); 60 | 61 | return new Uri("file://" + fileSystemPath); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/LSP.Client.Tests/ConnectionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace LSP.Client.Tests 6 | { 7 | using Dispatcher; 8 | using Protocol; 9 | 10 | /// 11 | /// Tests for . 12 | /// 13 | public class ConnectionTests 14 | : PipeServerTestBase 15 | { 16 | /// 17 | /// Create a new test suite. 18 | /// 19 | /// 20 | /// Output for the current test. 21 | /// 22 | public ConnectionTests(ITestOutputHelper testOutput) 23 | : base(testOutput) 24 | { 25 | } 26 | 27 | /// 28 | /// Verify that a server can handle an empty notification from a client . 29 | /// 30 | [Fact(DisplayName = "Server connection can handle empty notification from client")] 31 | public async Task Client_HandleEmptyNotification_Success() 32 | { 33 | TaskCompletionSource testCompletion = new TaskCompletionSource(); 34 | 35 | LspConnection serverConnection = await CreateServerConnection(); 36 | LspConnection clientConnection = await CreateClientConnection(); 37 | 38 | LspDispatcher serverDispatcher = new LspDispatcher(); 39 | serverDispatcher.HandleEmptyNotification("test", () => 40 | { 41 | Log.Information("Got notification."); 42 | 43 | testCompletion.SetResult(null); 44 | }); 45 | serverConnection.Connect(serverDispatcher); 46 | 47 | clientConnection.Connect(new LspDispatcher()); 48 | clientConnection.SendEmptyNotification("test"); 49 | 50 | await testCompletion.Task; 51 | 52 | clientConnection.Disconnect(flushOutgoing: true); 53 | serverConnection.Disconnect(); 54 | 55 | await Task.WhenAll(clientConnection.HasHasDisconnected, serverConnection.HasHasDisconnected); 56 | } 57 | 58 | /// 59 | /// Verify that a client can handle an empty notification from a server . 60 | /// 61 | [Fact(DisplayName = "Client connection can handle empty notification from server")] 62 | public async Task Server_HandleEmptyNotification_Success() 63 | { 64 | TaskCompletionSource testCompletion = new TaskCompletionSource(); 65 | 66 | LspConnection clientConnection = await CreateClientConnection(); 67 | LspConnection serverConnection = await CreateServerConnection(); 68 | 69 | LspDispatcher clientDispatcher = new LspDispatcher(); 70 | clientDispatcher.HandleEmptyNotification("test", () => 71 | { 72 | Log.Information("Got notification."); 73 | 74 | testCompletion.SetResult(null); 75 | }); 76 | clientConnection.Connect(clientDispatcher); 77 | 78 | serverConnection.Connect(new LspDispatcher()); 79 | serverConnection.SendEmptyNotification("test"); 80 | 81 | await testCompletion.Task; 82 | 83 | serverConnection.Disconnect(flushOutgoing: true); 84 | clientConnection.Disconnect(); 85 | 86 | await Task.WhenAll(clientConnection.HasHasDisconnected, serverConnection.HasHasDisconnected); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/LSP.Client.Tests/LSP.Client.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/LSP.Client.Tests/Logging/SerilogTestOutputExtensions.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using Serilog.Configuration; 3 | using Serilog.Core; 4 | using System; 5 | using Xunit.Abstractions; 6 | 7 | namespace LSP.Client.Tests.Logging 8 | { 9 | /// 10 | /// Extension methods for configuring Serilog. 11 | /// 12 | public static class SerilogTestOutputExtensions 13 | { 14 | /// 15 | /// Write log events to Xunit test output. 16 | /// 17 | /// 18 | /// The logger sink configuration. 19 | /// 20 | /// 21 | /// The test output to which events will be logged. 22 | /// 23 | /// 24 | /// An optional to control logging. 25 | /// 26 | /// 27 | /// The logger configuration. 28 | /// 29 | public static LoggerConfiguration TestOutput(this LoggerSinkConfiguration loggerSinkConfiguration, ITestOutputHelper testOutput, LoggingLevelSwitch levelSwitch = null) 30 | { 31 | if (loggerSinkConfiguration == null) 32 | throw new ArgumentNullException(nameof(loggerSinkConfiguration)); 33 | 34 | if (testOutput == null) 35 | throw new ArgumentNullException(nameof(testOutput)); 36 | 37 | return loggerSinkConfiguration.Sink( 38 | new TestOutputLoggingSink(testOutput, 39 | levelSwitch: levelSwitch ?? new LoggingLevelSwitch() 40 | ) 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/LSP.Client.Tests/Logging/TestOutputSink.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Core; 2 | using System; 3 | using System.Linq; 4 | using Serilog.Events; 5 | using Xunit.Abstractions; 6 | 7 | namespace LSP.Client.Tests.Logging 8 | { 9 | /// 10 | /// A Serilog logging sink that sends log events to Xunit test output. 11 | /// 12 | public class TestOutputLoggingSink 13 | : ILogEventSink 14 | { 15 | /// 16 | /// The test output to which events will be logged. 17 | /// 18 | readonly ITestOutputHelper _testOutput; 19 | 20 | /// 21 | /// The that controls logging. 22 | /// 23 | readonly LoggingLevelSwitch _levelSwitch; 24 | 25 | /// 26 | /// Create a new test output event sink. 27 | /// 28 | /// 29 | /// The test output to which events will be logged. 30 | /// 31 | /// 32 | /// The that controls logging. 33 | /// 34 | public TestOutputLoggingSink(ITestOutputHelper testOutput, LoggingLevelSwitch levelSwitch) 35 | { 36 | if (testOutput == null) 37 | throw new ArgumentNullException(nameof(testOutput)); 38 | 39 | if (levelSwitch == null) 40 | throw new ArgumentNullException(nameof(levelSwitch)); 41 | 42 | _testOutput = testOutput; 43 | _levelSwitch = levelSwitch; 44 | } 45 | 46 | /// 47 | /// Emit a log event. 48 | /// 49 | /// 50 | /// The log event information. 51 | /// 52 | public void Emit(LogEvent logEvent) 53 | { 54 | if (logEvent.Level < _levelSwitch.MinimumLevel) 55 | return; 56 | 57 | string sourceContext = String.Empty; 58 | if (logEvent.Properties.TryGetValue("SourceContext", out LogEventPropertyValue sourceContextValue)) 59 | { 60 | if (sourceContextValue is ScalarValue scalarValue) 61 | sourceContext = scalarValue.Value?.ToString() ?? String.Empty; 62 | else 63 | sourceContext = sourceContextValue.ToString(); 64 | } 65 | 66 | // Trim off namespace, if possible. 67 | string[] sourceContextSegments = sourceContext.Split('.'); 68 | sourceContext = sourceContextSegments[sourceContextSegments.Length - 1]; 69 | 70 | string prefix = !String.IsNullOrWhiteSpace(sourceContext) 71 | ? $"[{sourceContext}/{logEvent.Level}] " 72 | : $"[{logEvent.Level}] "; 73 | 74 | string message = prefix + logEvent.RenderMessage(); 75 | if (logEvent.Exception != null) 76 | message += "\n" + logEvent.Exception.ToString(); 77 | 78 | _testOutput.WriteLine(message); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/LSP.Client.Tests/PipeServerTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Xunit.Abstractions; 5 | 6 | namespace LSP.Client.Tests 7 | { 8 | using Dispatcher; 9 | using Processes; 10 | using Protocol; 11 | 12 | /// 13 | /// The base class for test suites that use a . 14 | /// 15 | public abstract class PipeServerTestBase 16 | : TestBase 17 | { 18 | /// 19 | /// The used to connect client and server streams. 20 | /// 21 | readonly NamedPipeServerProcess _serverProcess; 22 | 23 | /// 24 | /// Create a new . 25 | /// 26 | /// 27 | /// Output for the current test. 28 | /// 29 | protected PipeServerTestBase(ITestOutputHelper testOutput) 30 | : base(testOutput) 31 | { 32 | _serverProcess = new NamedPipeServerProcess(Guid.NewGuid().ToString("N"), Log); 33 | Disposal.Add(_serverProcess); 34 | } 35 | 36 | /// 37 | /// The workspace root path. 38 | /// 39 | protected virtual string WorkspaceRoot => Path.GetDirectoryName(GetType().Assembly.Location); 40 | 41 | /// 42 | /// The client's output stream (server reads from this). 43 | /// 44 | protected Stream ClientOutputStream => _serverProcess.ClientOutputStream; 45 | 46 | /// 47 | /// The client's input stream (server writes to this). 48 | /// 49 | protected Stream ClientInputStream => _serverProcess.ClientInputStream; 50 | 51 | /// 52 | /// The server's output stream (client reads from this). 53 | /// 54 | protected Stream ServerOutputStream => _serverProcess.ServerOutputStream; 55 | 56 | /// 57 | /// The server's input stream (client writes to this). 58 | /// 59 | protected Stream ServerInputStream => _serverProcess.ServerInputStream; 60 | 61 | /// 62 | /// Create a connected to the test's . 63 | /// 64 | /// 65 | /// Automatically initialise the client? 66 | /// 67 | /// Default is true. 68 | /// 69 | /// 70 | /// The . 71 | /// 72 | protected async Task CreateClient(bool initialize = true) 73 | { 74 | if (!_serverProcess.IsRunning) 75 | await StartServer(); 76 | 77 | await _serverProcess.HasStarted; 78 | 79 | LanguageClient client = new LanguageClient(Log, _serverProcess); 80 | Disposal.Add(client); 81 | 82 | if (initialize) 83 | await client.Initialize(WorkspaceRoot); 84 | 85 | return client; 86 | } 87 | 88 | /// 89 | /// Create a that uses the client ends of the the test's streams. 90 | /// 91 | /// 92 | /// The . 93 | /// 94 | protected async Task CreateClientConnection() 95 | { 96 | if (!_serverProcess.IsRunning) 97 | await StartServer(); 98 | 99 | await _serverProcess.HasStarted; 100 | 101 | LspConnection connection = new LspConnection(Log, input: ServerOutputStream, output: ServerInputStream); 102 | Disposal.Add(connection); 103 | 104 | return connection; 105 | } 106 | 107 | /// 108 | /// Create a that uses the server ends of the the test's streams. 109 | /// 110 | /// 111 | /// The . 112 | /// 113 | protected async Task CreateServerConnection() 114 | { 115 | if (!_serverProcess.IsRunning) 116 | await StartServer(); 117 | 118 | await _serverProcess.HasStarted; 119 | 120 | LspConnection connection = new LspConnection(Log, input: ClientOutputStream, output: ClientInputStream); 121 | Disposal.Add(connection); 122 | 123 | return connection; 124 | } 125 | 126 | /// 127 | /// Called to start the server process. 128 | /// 129 | /// 130 | /// A representing the operation. 131 | /// 132 | protected virtual Task StartServer() => _serverProcess.Start(); 133 | 134 | /// 135 | /// Called to stop the server process. 136 | /// 137 | /// 138 | /// A representing the operation. 139 | /// 140 | protected virtual Task StopServer() => _serverProcess.Stop(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /test/LSP.Client.Tests/PipeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipes; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | using System.Threading; 6 | using Xunit.Abstractions; 7 | 8 | namespace LSP.Client.Tests 9 | { 10 | public class PipeTests 11 | : TestBase 12 | { 13 | public PipeTests(ITestOutputHelper testOutput) 14 | : base(testOutput) 15 | { 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/LSP.Client.Tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using Serilog.Context; 3 | using Serilog.Core; 4 | using Serilog.Events; 5 | using System; 6 | using System.Reactive.Disposables; 7 | using System.Reflection; 8 | using System.Threading; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | 12 | namespace LSP.Client.Tests 13 | { 14 | using Logging; 15 | 16 | /// 17 | /// The base class for test suites. 18 | /// 19 | public abstract class TestBase 20 | : IDisposable 21 | { 22 | /// 23 | /// Create a new test-suite. 24 | /// 25 | /// 26 | /// Output for the current test. 27 | /// 28 | protected TestBase(ITestOutputHelper testOutput) 29 | { 30 | if (testOutput == null) 31 | throw new ArgumentNullException(nameof(testOutput)); 32 | 33 | // We *must* have a synchronisation context for the test, or we'll see random deadlocks. 34 | SynchronizationContext.SetSynchronizationContext( 35 | new SynchronizationContext() 36 | ); 37 | 38 | TestOutput = testOutput; 39 | 40 | // Redirect component logging to Serilog. 41 | Log = 42 | new LoggerConfiguration() 43 | .MinimumLevel.Verbose() 44 | .Enrich.FromLogContext() 45 | .WriteTo.Debug( 46 | restrictedToMinimumLevel: LogEventLevel.Verbose 47 | ) 48 | .WriteTo.TestOutput(TestOutput, LogLevelSwitch) 49 | .CreateLogger(); 50 | 51 | // Ugly hack to get access to the current test. 52 | CurrentTest = (ITest) 53 | TestOutput.GetType() 54 | .GetField("test", BindingFlags.NonPublic | BindingFlags.Instance) 55 | .GetValue(TestOutput); 56 | 57 | Assert.True(CurrentTest != null, "Cannot retrieve current test from ITestOutputHelper."); 58 | 59 | Disposal.Add( 60 | LogContext.PushProperty("TestName", CurrentTest.DisplayName) 61 | ); 62 | } 63 | 64 | /// 65 | /// Finaliser for . 66 | /// 67 | ~TestBase() 68 | { 69 | Dispose(false); 70 | } 71 | 72 | /// 73 | /// Dispose of resources being used by the test suite. 74 | /// 75 | public void Dispose() 76 | { 77 | Dispose(true); 78 | GC.SuppressFinalize(this); 79 | } 80 | 81 | /// 82 | /// Dispose of resources being used by the test suite. 83 | /// 84 | /// 85 | /// Explicit disposal? 86 | /// 87 | protected virtual void Dispose(bool disposing) 88 | { 89 | if (disposing) 90 | { 91 | try 92 | { 93 | Disposal.Dispose(); 94 | } 95 | finally 96 | { 97 | if (Log is IDisposable logDisposal) 98 | logDisposal.Dispose(); 99 | } 100 | } 101 | } 102 | 103 | /// 104 | /// A representing resources used by the test. 105 | /// 106 | protected CompositeDisposable Disposal { get; } = new CompositeDisposable(); 107 | 108 | /// 109 | /// Output for the current test. 110 | /// 111 | protected ITestOutputHelper TestOutput { get; } 112 | 113 | /// 114 | /// A representing the current test. 115 | /// 116 | protected ITest CurrentTest { get; } 117 | 118 | /// 119 | /// The Serilog logger for the current test. 120 | /// 121 | protected ILogger Log { get; } 122 | 123 | /// 124 | /// A switch to control the logging level for the current test. 125 | /// 126 | protected LoggingLevelSwitch LogLevelSwitch { get; } = new LoggingLevelSwitch(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/TestCommon.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | IDE0016 4 | false 5 | 6 | --------------------------------------------------------------------------------