DCD/common/src/dcd/common/messages.d

352 lines
8.0 KiB
D

/**
* This file is part of DCD, a development tool for the D programming language.
* Copyright (C) 2014 Brian Schott
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
module dcd.common.messages;
import std.socket;
import msgpack;
import core.time : dur;
/**
* The type of completion list being returned
*/
enum CompletionType : string
{
/**
* The completion list contains a listing of identifier/kind pairs.
*/
identifiers = "identifiers",
/**
* The auto-completion list consists of a listing of functions and their
* parameters.
*/
calltips = "calltips",
/**
* The response contains the location of a symbol declaration.
*/
location = "location",
/**
* The response contains documentation comments for the symbol.
*/
ddoc = "ddoc",
}
/**
* Request kind
*/
enum RequestKind : ushort
{
// dfmt off
uninitialized = 0b00000000_00000000,
/// Autocompletion
autocomplete = 0b00000000_00000001,
/// Clear the completion cache
clearCache = 0b00000000_00000010,
/// Add import directory to server
addImport = 0b00000000_00000100,
/// Shut down the server
shutdown = 0b00000000_00001000,
/// Get declaration location of given symbol
symbolLocation = 0b00000000_00010000,
/// Get the doc comments for the symbol
doc = 0b00000000_00100000,
/// Query server status
query = 0b00000000_01000000,
/// Search for symbol
search = 0b00000000_10000000,
/// List import directories
listImports = 0b00000001_00000000,
/// Local symbol usage
localUse = 0b00000010_00000000,
/// Remove import directory from server
removeImport = 0b00000100_00000000,
/// Get inlay hints
inlayHints = 0b00001000_00000000,
/// These request kinds require source code and won't be executed if there
/// is no source sent
requiresSourceCode = autocomplete | doc | symbolLocation | search | localUse | inlayHints,
// dfmt on
}
/**
* Autocompletion request message
*/
struct AutocompleteRequest
{
/**
* File name used for error reporting
*/
string fileName;
/**
* Command coming from the client
*/
RequestKind kind;
/**
* Paths to be searched for import files
*/
string[] importPaths;
/**
* The source code to auto complete
*/
ubyte[] sourceCode;
/**
* The cursor position
*/
size_t cursorPosition;
/**
* Name of symbol searched for
*/
string searchName;
}
/**
* Autocompletion response message
*/
struct AutocompleteResponse
{
static struct Completion
{
/**
* The name of the symbol for a completion, for calltips just the function name.
*/
string identifier;
/**
* The kind of the item. Will be char.init for calltips.
*/
ubyte kind;
/**
* Definition for a symbol for a completion including attributes or the arguments for calltips.
*/
string definition;
/**
* The path to the file that contains the symbol.
*/
string symbolFilePath;
/**
* The byte offset at which the symbol is located or symbol location for symbol searches.
*/
size_t symbolLocation;
/**
* Documentation associated with this symbol.
*/
string documentation;
// when changing the behavior here, update README.md
/**
* For variables, fields, globals, constants: resolved type or empty if unresolved.
* For functions: resolved return type or empty if unresolved.
* For constructors: may be struct/class name or empty in any case.
* Otherwise (probably) empty.
*/
string typeOf;
}
/**
* The autocompletion type. (Parameters or identifier)
*/
string completionType;
/**
* The path to the file that contains the symbol.
*/
string symbolFilePath;
/**
* The byte offset at which the symbol is located.
*/
size_t symbolLocation;
/**
* The completions
*/
Completion[] completions;
/**
* Import paths that are registered by the server.
*/
string[] importPaths;
/**
* Symbol identifier
*/
ulong symbolIdentifier;
/**
* Creates an empty acknowledgement response
*/
static AutocompleteResponse ack()
{
AutocompleteResponse response;
response.completionType = "ack";
return response;
}
}
/**
* Returns: true on success
*/
bool sendRequest(Socket socket, AutocompleteRequest request)
{
ubyte[] message = msgpack.pack(request);
ubyte[] messageBuffer = new ubyte[message.length + message.length.sizeof];
auto messageLength = message.length;
messageBuffer[0 .. size_t.sizeof] = (cast(ubyte*) &messageLength)[0 .. size_t.sizeof];
messageBuffer[size_t.sizeof .. $] = message[];
size_t i;
while (i < messageBuffer.length)
{
auto sent = socket.send(messageBuffer[i .. $]);
if (sent == Socket.ERROR)
return false;
i += sent;
}
return true;
}
/**
* Gets the response from the server
*/
AutocompleteResponse getResponse(Socket socket)
{
ubyte[1024 * 16] buffer;
ptrdiff_t bytesReceived = 0;
auto unpacker = StreamingUnpacker([]);
do {
bytesReceived = socket.receive(buffer);
if (bytesReceived == Socket.ERROR)
throw new Exception(lastSocketError);
if (bytesReceived == 0)
break;
unpacker.feed(buffer[0..bytesReceived]);
} while (true);
if (unpacker.size == 0)
throw new Exception("Server closed the connection, 0 bytes received");
if (!unpacker.execute())
throw new Exception("Could not unpack the response");
return unpacker.purge().value.as!AutocompleteResponse;
}
/**
* Returns: true if a server instance is running
* Params:
* useTCP = `true` to check a TCP port, `false` for UNIX domain socket
* socketFile = the file name for the UNIX domain socket
* port = the TCP port
*/
bool serverIsRunning(bool useTCP, string socketFile, ushort port)
{
try {
AutocompleteRequest request;
request.kind = RequestKind.query;
Socket socket;
scope (exit)
{
socket.shutdown(SocketShutdown.BOTH);
socket.close();
}
version(Windows) useTCP = true;
if (useTCP)
{
socket = new TcpSocket(AddressFamily.INET);
socket.connect(new InternetAddress("localhost", port));
}
else
{
version(Windows) {} else
{
socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
socket.connect(new UnixAddress(socketFile));
}
}
socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(5));
socket.blocking = true;
if (sendRequest(socket, request))
return getResponse(socket).completionType == "ack";
else
return false;
}
catch (Exception _) {
return false;
}
}
/// Escapes \n, \t and \ in the string. If `single` is true \t won't be escaped.
/// Params:
/// s = the string to escape
/// single = if true, `s` is already tab separated and only needs to be escape new lines.
/// Otherwise `s` is treated as one value that is meant to be printed tab separated with other values.
/// Returns: An escaped string that is safe to print to the console so it can be read line by line
string escapeConsoleOutputString(string s, bool single = false)
{
import std.array : Appender;
Appender!(char[]) app;
void putChar(char c)
{
switch (c)
{
case '\\':
app.put('\\');
app.put('\\');
break;
case '\n':
app.put('\\');
app.put('n');
break;
case '\t':
if (single)
goto default;
else
{
app.put('\\');
app.put('t');
break;
}
default:
app.put(c);
break;
}
}
foreach (char c; s)
putChar(c);
return app.data.idup;
}
/// Joins string arguments with tabs and escapes them
string makeTabSeparated(string[] args...)
{
import std.algorithm : map;
import std.array : join;
return args.map!(a => a.escapeConsoleOutputString).join("\t");
}