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 }