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