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.client.client; 20 21 import std.socket; 22 import std.stdio; 23 import std.getopt; 24 import std.array; 25 import std.process; 26 import std.algorithm; 27 import std.path; 28 import std.file; 29 import std.conv; 30 import std.string; 31 import std.experimental.logger; 32 33 import dcd.common.messages; 34 import dcd.common.dcd_version; 35 import dcd.common.socket; 36 37 int main(string[] args) 38 { 39 sharedLog.fatalHandler = () {}; 40 41 size_t cursorPos = size_t.max; 42 string[] importPaths; 43 ushort port; 44 bool help; 45 bool shutdown; 46 bool clearCache; 47 bool symbolLocation; 48 bool doc; 49 bool query; 50 bool printVersion; 51 bool listImports; 52 bool getIdentifier; 53 bool localUse; 54 bool fullOutput; 55 string search; 56 version(Windows) 57 { 58 bool useTCP = true; 59 string socketFile; 60 } 61 else 62 { 63 bool useTCP = false; 64 string socketFile = generateSocketName(); 65 } 66 67 try 68 { 69 getopt(args, "cursorPos|c", &cursorPos, "I", &importPaths, 70 "port|p", &port, "help|h", &help, "shutdown", &shutdown, 71 "clearCache", &clearCache, "symbolLocation|l", &symbolLocation, 72 "doc|d", &doc, "query|status|q", &query, "search|s", &search, 73 "version", &printVersion, "listImports", &listImports, 74 "tcp", &useTCP, "socketFile", &socketFile, 75 "getIdentifier", &getIdentifier, 76 "localUse|u", &localUse, "extended|x", &fullOutput); 77 } 78 catch (ConvException e) 79 { 80 fatal(e.msg); 81 printHelp(args[0]); 82 return 1; 83 } 84 85 AutocompleteRequest request; 86 87 if (help) 88 { 89 printHelp(args[0]); 90 return 0; 91 } 92 93 if (printVersion) 94 { 95 version (Windows) 96 writeln(DCD_VERSION); 97 else version(built_with_dub) 98 writeln(DCD_VERSION); 99 else 100 write(DCD_VERSION, " ", GIT_HASH); 101 return 0; 102 } 103 104 version (Windows) if (socketFile !is null) 105 { 106 fatal("UNIX domain sockets not supported on Windows"); 107 return 1; 108 } 109 110 // If the user specified a port number, assume that they wanted a TCP 111 // connection. Otherwise set the port number to the default and let the 112 // useTCP flag deterimen what to do later. 113 if (port != 0) 114 useTCP = true; 115 else 116 port = DEFAULT_PORT_NUMBER; 117 118 if (useTCP) 119 socketFile = null; 120 121 if (query) 122 { 123 if (serverIsRunning(useTCP, socketFile, port)) 124 { 125 writeln("Server is running"); 126 return 0; 127 } 128 else 129 { 130 writeln("Server is not running"); 131 return 1; 132 } 133 } 134 else if (shutdown || clearCache) 135 { 136 if (shutdown) 137 request.kind = RequestKind.shutdown; 138 else if (clearCache) 139 request.kind = RequestKind.clearCache; 140 Socket socket = createSocket(socketFile, port); 141 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 142 return sendRequest(socket, request) ? 0 : 1; 143 } 144 else if (importPaths.length > 0) 145 { 146 request.kind |= RequestKind.addImport; 147 request.importPaths = importPaths.map!(a => absolutePath(a)).array; 148 if (cursorPos == size_t.max) 149 { 150 Socket socket = createSocket(socketFile, port); 151 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 152 if (!sendRequest(socket, request)) 153 return 1; 154 return 0; 155 } 156 } 157 else if (listImports) 158 { 159 request.kind |= RequestKind.listImports; 160 Socket socket = createSocket(socketFile, port); 161 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 162 sendRequest(socket, request); 163 AutocompleteResponse response = getResponse(socket); 164 printImportList(response); 165 return 0; 166 } 167 else if (search == null && cursorPos == size_t.max) 168 { 169 // cursor position is a required argument 170 printHelp(args[0]); 171 return 1; 172 } 173 174 // Read in the source 175 immutable bool usingStdin = args.length <= 1; 176 string fileName = usingStdin ? "stdin" : args[1]; 177 if (!usingStdin && !exists(args[1])) 178 { 179 stderr.writefln("%s does not exist", args[1]); 180 return 1; 181 } 182 ubyte[] sourceCode; 183 if (usingStdin) 184 { 185 ubyte[4096] buf; 186 while (true) 187 { 188 auto b = stdin.rawRead(buf); 189 if (b.length == 0) 190 break; 191 sourceCode ~= b; 192 } 193 } 194 else 195 { 196 if (!exists(args[1])) 197 { 198 stderr.writeln("Could not find ", args[1]); 199 return 1; 200 } 201 File f = File(args[1]); 202 sourceCode = uninitializedArray!(ubyte[])(to!size_t(f.size)); 203 f.rawRead(sourceCode); 204 } 205 206 request.fileName = fileName; 207 request.importPaths = importPaths; 208 request.sourceCode = sourceCode; 209 request.cursorPosition = cursorPos; 210 request.searchName = search; 211 212 if (symbolLocation | getIdentifier) 213 request.kind |= RequestKind.symbolLocation; 214 else if (doc) 215 request.kind |= RequestKind.doc; 216 else if (search) 217 request.kind |= RequestKind.search; 218 else if(localUse) 219 request.kind |= RequestKind.localUse; 220 else 221 request.kind |= RequestKind.autocomplete; 222 223 // Send message to server 224 Socket socket = createSocket(socketFile, port); 225 scope (exit) { socket.shutdown(SocketShutdown.BOTH); socket.close(); } 226 if (!sendRequest(socket, request)) 227 return 1; 228 229 AutocompleteResponse response = getResponse(socket); 230 231 if (symbolLocation) 232 printLocationResponse(response); 233 else if (getIdentifier) 234 printIdentifierResponse(response); 235 else if (doc) 236 printDocResponse(response); 237 else if (search !is null) 238 printSearchResponse(response); 239 else if (localUse) 240 printLocalUse(response); 241 else 242 printCompletionResponse(response, fullOutput); 243 244 return 0; 245 } 246 247 private: 248 249 void printHelp(string programName) 250 { 251 writefln( 252 ` 253 Usage: %1$s [Options] [FILENAME] 254 255 A file name is optional. If it is given, autocomplete information will be 256 given for the file specified. If it is missing, input will be read from 257 stdin instead. 258 259 Source code is assumed to be UTF-8 encoded and must not exceed 4 megabytes. 260 261 Options: 262 --help | -h 263 Displays this help message 264 265 --cursorPos | -c position 266 Provides auto-completion at the given cursor position. The cursor 267 position is measured in bytes from the beginning of the source code. 268 269 --clearCache 270 Instructs the server to clear out its autocompletion cache. 271 272 --shutdown 273 Instructs the server to shut down. 274 275 --symbolLocation | -l 276 Get the file name and position that the symbol at the cursor location 277 was defined. 278 279 --doc | -d 280 Gets documentation comments associated with the symbol at the cursor 281 location. 282 283 --search | -s symbolName 284 Searches for symbolName in both stdin / the given file name as well as 285 others files cached by the server. 286 287 --localUse | -u 288 Searches for all the uses of the symbol at the cursor location 289 in the given filename (or stdin). 290 291 --extended | -x 292 Includes more information with a slightly different format for 293 calltips when autocompleting. 294 295 --query | -q | --status 296 Query the server statis. Returns 0 if the server is running. Returns 297 1 if the server could not be contacted. 298 299 -I PATH 300 Instructs the server to add PATH to its list of paths searched for 301 imported modules. 302 303 --version 304 Prints the version number and then exits. 305 306 --port PORTNUMBER | -p PORTNUMBER 307 Uses PORTNUMBER to communicate with the server instead of the default 308 port 9166. Only used on Windows or when the --tcp option is set. 309 310 --tcp 311 Send requests on a TCP socket instead of a UNIX domain socket. This 312 switch has no effect on Windows. 313 314 --socketFile FILENAME 315 Use the given FILENAME as the path to the UNIX domain socket. Using 316 this switch is an error on Windows.`, programName); 317 } 318 319 Socket createSocket(string socketFile, ushort port) 320 { 321 import core.time : dur; 322 323 Socket socket; 324 if (socketFile is null) 325 { 326 socket = new TcpSocket(AddressFamily.INET); 327 socket.connect(new InternetAddress("localhost", port)); 328 } 329 else 330 { 331 version(Windows) 332 { 333 // should never be called with non-null socketFile on Windows 334 assert(false); 335 } 336 else 337 { 338 socket = new Socket(AddressFamily.UNIX, SocketType.STREAM); 339 socket.connect(new UnixAddress(socketFile)); 340 } 341 } 342 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(5)); 343 socket.blocking = true; 344 return socket; 345 } 346 347 void printDocResponse(ref const AutocompleteResponse response) 348 { 349 import std.algorithm : each; 350 response.completions.each!(a => a.documentation.escapeConsoleOutputString(true).writeln); 351 } 352 353 void printIdentifierResponse(ref const AutocompleteResponse response) 354 { 355 if (response.completions.length == 0) 356 return; 357 writeln(makeTabSeparated(response.completions[0].identifier, response.symbolIdentifier.to!string)); 358 } 359 360 void printLocationResponse(ref const AutocompleteResponse response) 361 { 362 if (response.symbolFilePath is null) 363 writeln("Not found"); 364 else 365 writeln(makeTabSeparated(response.symbolFilePath, response.symbolLocation.to!string)); 366 } 367 368 void printCompletionResponse(ref const AutocompleteResponse response, bool extended) 369 { 370 if (response.completions.length > 0) 371 { 372 writeln(response.completionType); 373 auto app = appender!(string[])(); 374 if (response.completionType == CompletionType.identifiers || extended) 375 { 376 foreach (ref completion; response.completions) 377 { 378 if (extended) 379 app.put(makeTabSeparated( 380 completion.identifier, 381 completion.kind == char.init ? "" : "" ~ completion.kind, 382 completion.definition, 383 completion.symbolFilePath.length ? completion.symbolFilePath ~ " " ~ completion.symbolLocation.to!string : "", 384 completion.documentation 385 )); 386 else 387 app.put(makeTabSeparated(completion.identifier, "" ~ completion.kind)); 388 } 389 } 390 else 391 { 392 foreach (completion; response.completions) 393 app.put(completion.definition); 394 } 395 // Deduplicate overloaded methods 396 foreach (line; app.data.sort().uniq) 397 writeln(line); 398 } 399 } 400 401 void printSearchResponse(const AutocompleteResponse response) 402 { 403 foreach(ref completion; response.completions) 404 writeln(makeTabSeparated(completion.identifier, "" ~ completion.kind, completion.symbolLocation.to!string)); 405 } 406 407 void printLocalUse(const AutocompleteResponse response) 408 { 409 if (response.symbolFilePath.length) 410 { 411 writeln(makeTabSeparated(response.symbolFilePath, response.symbolLocation.to!string)); 412 foreach(loc; response.completions) 413 writeln(loc.symbolLocation); 414 } 415 else write("00000"); 416 } 417 418 void printImportList(const AutocompleteResponse response) 419 { 420 import std.algorithm.iteration : each; 421 422 response.importPaths.each!(a => writeln(a)); 423 }