1 /** 2 * This file is part of DCD, a development tool for the D programming language. 3 * Copyright (C) 2014 Brian Schott 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 */ 18 19 module dcd.common.messages; 20 21 import std.socket; 22 import msgpack; 23 import core.time : dur; 24 25 /** 26 * The type of completion list being returned 27 */ 28 enum CompletionType : string 29 { 30 /** 31 * The completion list contains a listing of identifier/kind pairs. 32 */ 33 identifiers = "identifiers", 34 35 /** 36 * The auto-completion list consists of a listing of functions and their 37 * parameters. 38 */ 39 calltips = "calltips", 40 41 /** 42 * The response contains the location of a symbol declaration. 43 */ 44 location = "location", 45 46 /** 47 * The response contains documentation comments for the symbol. 48 */ 49 ddoc = "ddoc", 50 } 51 52 /** 53 * Request kind 54 */ 55 enum RequestKind : ushort 56 { 57 // dfmt off 58 uninitialized = 0b00000000_00000000, 59 /// Autocompletion 60 autocomplete = 0b00000000_00000001, 61 /// Clear the completion cache 62 clearCache = 0b00000000_00000010, 63 /// Add import directory to server 64 addImport = 0b00000000_00000100, 65 /// Shut down the server 66 shutdown = 0b00000000_00001000, 67 /// Get declaration location of given symbol 68 symbolLocation = 0b00000000_00010000, 69 /// Get the doc comments for the symbol 70 doc = 0b00000000_00100000, 71 /// Query server status 72 query = 0b00000000_01000000, 73 /// Search for symbol 74 search = 0b00000000_10000000, 75 /// List import directories 76 listImports = 0b00000001_00000000, 77 /// local symbol usage 78 localUse = 0b00000010_00000000, 79 // dfmt on 80 } 81 82 /** 83 * Autocompletion request message 84 */ 85 struct AutocompleteRequest 86 { 87 /** 88 * File name used for error reporting 89 */ 90 string fileName; 91 92 /** 93 * Command coming from the client 94 */ 95 RequestKind kind; 96 97 /** 98 * Paths to be searched for import files 99 */ 100 string[] importPaths; 101 102 /** 103 * The source code to auto complete 104 */ 105 ubyte[] sourceCode; 106 107 /** 108 * The cursor position 109 */ 110 size_t cursorPosition; 111 112 /** 113 * Name of symbol searched for 114 */ 115 string searchName; 116 } 117 118 /** 119 * Autocompletion response message 120 */ 121 struct AutocompleteResponse 122 { 123 static struct Completion 124 { 125 /** 126 * The name of the symbol for a completion, for calltips just the function name. 127 */ 128 string identifier; 129 /** 130 * The kind of the item. Will be char.init for calltips. 131 */ 132 ubyte kind; 133 /** 134 * Definition for a symbol for a completion including attributes or the arguments for calltips. 135 */ 136 string definition; 137 /** 138 * The path to the file that contains the symbol. 139 */ 140 string symbolFilePath; 141 /** 142 * The byte offset at which the symbol is located or symbol location for symbol searches. 143 */ 144 size_t symbolLocation; 145 /** 146 * Documentation associated with this symbol. 147 */ 148 string documentation; 149 150 deprecated("Use identifier (or definition for calltips) instead") string compatibilityContent() const 151 { 152 if (kind == ubyte.init) 153 return definition; 154 else 155 return identifier; 156 } 157 158 alias compatibilityContent this; 159 } 160 161 /** 162 * The autocompletion type. (Parameters or identifier) 163 */ 164 string completionType; 165 166 /** 167 * The path to the file that contains the symbol. 168 */ 169 string symbolFilePath; 170 171 /** 172 * The byte offset at which the symbol is located. 173 */ 174 size_t symbolLocation; 175 176 /** 177 * The completions 178 */ 179 Completion[] completions; 180 181 /** 182 * Import paths that are registered by the server. 183 */ 184 string[] importPaths; 185 186 /** 187 * Symbol identifier 188 */ 189 ulong symbolIdentifier; 190 191 deprecated("use completions[].documentation + escapeConsoleOutputString instead") string[] docComments() @property 192 { 193 string[] ret; 194 foreach (ref completion; completions) 195 ret ~= completion.documentation.escapeConsoleOutputString(true); 196 return ret; 197 } 198 199 deprecated("use completions[].kind instead") char[] completionKinds() @property 200 { 201 char[] ret; 202 foreach (ref completion; completions) 203 ret ~= completion.kind; 204 return ret; 205 } 206 207 deprecated("use completions[].symbolLocation instead") size_t[] locations() @property 208 { 209 size_t[] ret; 210 foreach (ref completion; completions) 211 ret ~= completion.symbolLocation; 212 return ret; 213 } 214 215 /** 216 * Creates an empty acknowledgement response 217 */ 218 static AutocompleteResponse ack() 219 { 220 AutocompleteResponse response; 221 response.completionType = "ack"; 222 return response; 223 } 224 } 225 226 /** 227 * Returns: true on success 228 */ 229 bool sendRequest(Socket socket, AutocompleteRequest request) 230 { 231 ubyte[] message = msgpack.pack(request); 232 ubyte[] messageBuffer = new ubyte[message.length + message.length.sizeof]; 233 auto messageLength = message.length; 234 messageBuffer[0 .. size_t.sizeof] = (cast(ubyte*) &messageLength)[0 .. size_t.sizeof]; 235 messageBuffer[size_t.sizeof .. $] = message[]; 236 return socket.send(messageBuffer) == messageBuffer.length; 237 } 238 239 /** 240 * Gets the response from the server 241 */ 242 AutocompleteResponse getResponse(Socket socket) 243 { 244 ubyte[1024 * 16] buffer; 245 ptrdiff_t bytesReceived = 0; 246 auto unpacker = StreamingUnpacker([]); 247 248 do { 249 bytesReceived = socket.receive(buffer); 250 if (bytesReceived == Socket.ERROR) 251 throw new Exception(lastSocketError); 252 if (bytesReceived == 0) 253 break; 254 unpacker.feed(buffer[0..bytesReceived]); 255 } while (bytesReceived == buffer.length); 256 257 if (unpacker.size == 0) 258 throw new Exception("Server closed the connection, 0 bytes received"); 259 260 if (!unpacker.execute()) 261 throw new Exception("Could not unpack the response"); 262 263 return unpacker.purge().value.as!AutocompleteResponse; 264 } 265 266 /** 267 * Returns: true if a server instance is running 268 * Params: 269 * useTCP = `true` to check a TCP port, `false` for UNIX domain socket 270 * socketFile = the file name for the UNIX domain socket 271 * port = the TCP port 272 */ 273 bool serverIsRunning(bool useTCP, string socketFile, ushort port) 274 { 275 scope (failure) 276 return false; 277 AutocompleteRequest request; 278 request.kind = RequestKind.query; 279 Socket socket; 280 scope (exit) 281 { 282 socket.shutdown(SocketShutdown.BOTH); 283 socket.close(); 284 } 285 version(Windows) useTCP = true; 286 if (useTCP) 287 { 288 socket = new TcpSocket(AddressFamily.INET); 289 socket.connect(new InternetAddress("localhost", port)); 290 } 291 else 292 { 293 version(Windows) {} else 294 { 295 socket = new Socket(AddressFamily.UNIX, SocketType.STREAM); 296 socket.connect(new UnixAddress(socketFile)); 297 } 298 } 299 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(5)); 300 socket.blocking = true; 301 if (sendRequest(socket, request)) 302 return getResponse(socket).completionType == "ack"; 303 else 304 return false; 305 } 306 307 /// Escapes \n, \t and \ in the string. If `single` is true \t won't be escaped. 308 /// Params: 309 /// s = the string to escape 310 /// single = if true, `s` is already tab separated and only needs to be escape new lines. 311 /// Otherwise `s` is treated as one value that is meant to be printed tab separated with other values. 312 /// Returns: An escaped string that is safe to print to the console so it can be read line by line 313 string escapeConsoleOutputString(string s, bool single = false) 314 { 315 import std.array : Appender; 316 317 Appender!(char[]) app; 318 void putChar(char c) 319 { 320 switch (c) 321 { 322 case '\\': 323 app.put('\\'); 324 app.put('\\'); 325 break; 326 case '\n': 327 app.put('\\'); 328 app.put('n'); 329 break; 330 case '\t': 331 if (single) 332 goto default; 333 else 334 { 335 app.put('\\'); 336 app.put('t'); 337 break; 338 } 339 default: 340 app.put(c); 341 break; 342 } 343 } 344 345 foreach (char c; s) 346 putChar(c); 347 348 return app.data.idup; 349 } 350 351 /// Joins string arguments with tabs and escapes them 352 string makeTabSeparated(string[] args...) 353 { 354 import std.algorithm : map; 355 import std.array : join; 356 357 return args.map!(a => a.escapeConsoleOutputString).join("\t"); 358 }