LSP Client advances. Some LSPs are responding fine.

This commit is contained in:
Martín Lucas Golini
2022-10-31 22:14:09 -03:00
parent ab9bd56a49
commit bc5188ec28
15 changed files with 646 additions and 137 deletions

View File

@@ -10,6 +10,9 @@
namespace ecode {
#define CONTENT_LENGTH "Content-Length"
#define CONTENT_LENGTH_HEADER "Content-Length:"
static const char* MEMBER_ID = "id";
static const char* MEMBER_METHOD = "method";
static const char* MEMBER_PARAMS = "params";
@@ -18,8 +21,8 @@ static const char* MEMBER_VERSION = "version";
static const char* MEMBER_TEXT = "text";
static const char* MEMBER_LANGID = "languageId";
static const char* MEMBER_ERROR = "error";
// static const char* MEMBER_CODE = "code";
// static const char* MEMBER_MESSAGE = "message";
static const char* MEMBER_CODE = "code";
static const char* MEMBER_MESSAGE = "message";
static const char* MEMBER_RESULT = "result";
static const char* MEMBER_START = "start";
static const char* MEMBER_END = "end";
@@ -44,10 +47,10 @@ static const char* MEMBER_TARGET_SELECTION_RANGE = "targetSelectionRange";
// static const char* MEMBER_PREVIOUS_RESULT_ID = "previousResultId";
// static const char* MEMBER_QUERY = "query";
static json newRequest( const std::string& method, const json& params ) {
static json newRequest( const std::string& method, const json& params = {} ) {
json j;
j[MEMBER_METHOD] = method;
j[MEMBER_PARAMS] = params;
j[MEMBER_PARAMS] = params.empty() ? json() : params;
return j;
}
@@ -61,8 +64,8 @@ static json versionedTextDocumentIdentifier( const URI& document, int version =
static json textDocumentItem( const URI& document, const std::string& lang, const std::string& text,
int version ) {
auto map = versionedTextDocumentIdentifier( document, version );
map[MEMBER_TEXT] = text;
map[MEMBER_LANGID] = lang;
map[MEMBER_TEXT] = text;
return map;
}
@@ -73,33 +76,195 @@ static json textDocumentParams( const json& m ) {
static json textDocumentParams( const URI& document, int version = -1 ) {
return textDocumentParams( versionedTextDocumentIdentifier( document, version ) );
}
static json to_json( const TextPosition& pos ) {
static json toJson( const TextPosition& pos ) {
return json{ { MEMBER_LINE, pos.line() }, { MEMBER_CHARACTER, pos.column() } };
}
static json workspaceFolder( const LSPWorkspaceFolder& response ) {
return json{ { MEMBER_URI, response.uri.toString() }, { "name", response.name } };
}
static json toJson( const std::vector<LSPWorkspaceFolder>& l ) {
if ( l.empty() )
return json::array();
json result;
for ( const auto& e : l )
result.push_back( workspaceFolder( e ) );
return result;
}
static TextPosition parsePosition( const json& m ) {
auto line = m[MEMBER_LINE].get<int>();
auto column = m[MEMBER_CHARACTER].get<int>();
return { line, column };
}
static TextRange parseRange( const json& range ) {
auto startpos = parsePosition( range[MEMBER_START] );
auto endpos = parsePosition( range[MEMBER_END] );
return { startpos, endpos };
}
static LSPLocation parseLocation( const json& loc ) {
auto uri = URI( loc[MEMBER_URI].get<std::string>() );
auto range = parseRange( loc[MEMBER_RANGE] );
return { uri, range };
}
static LSPLocation parseLocationLink( const json& loc ) {
auto uri = URI( loc[MEMBER_TARGET_URI].get<std::string>() );
auto vrange = loc[MEMBER_TARGET_SELECTION_RANGE];
if ( vrange.is_null() )
vrange = loc[MEMBER_TARGET_RANGE];
auto range = parseRange( vrange );
return { uri, range };
}
static std::vector<LSPLocation> parseDocumentLocation( const json& result ) {
std::vector<LSPLocation> ret;
if ( result.is_array() ) {
const auto& locs = result;
for ( const auto& def : locs ) {
const auto& ob = def;
ret.push_back( parseLocation( ob ) );
if ( ret.back().uri.empty() )
ret.back() = parseLocationLink( ob );
}
} else if ( result.is_object() ) {
ret.push_back( parseLocation( result ) );
}
return ret;
}
static json changeWorkspaceFoldersParams( const std::vector<LSPWorkspaceFolder>& added,
const std::vector<LSPWorkspaceFolder>& removed ) {
json event;
event["added"] = toJson( added );
event["removed"] = toJson( removed );
return json{ { "event", event } };
}
static json textDocumentPositionParams( const URI& document, TextPosition pos ) {
auto params = textDocumentParams( document );
params[MEMBER_POSITION] = to_json( pos );
params[MEMBER_POSITION] = toJson( pos );
return params;
}
static void fromJson( std::vector<char>& trigger, const json& json ) {
if ( !json.empty() ) {
const auto triggersArray = json;
for ( const auto& t : triggersArray ) {
auto st = t.get<std::string>();
if ( st.length() )
trigger.push_back( st.at( 0 ) );
}
}
}
static void fromJson( LSPCompletionOptions& options, const json& json ) {
if ( !json.empty() && json.is_object() ) {
auto ob = json;
options.provider = true;
options.resolveProvider = ob["resolveProvider"].get<bool>();
fromJson( options.triggerCharacters, ob["triggerCharacters"] );
}
}
static void fromJson( LSPSignatureHelpOptions& options, const json& json ) {
if ( !json.empty() && json.is_object() ) {
auto ob = json;
options.provider = true;
fromJson( options.triggerCharacters, ob["triggerCharacters"] );
}
}
static void fromJson( LSPDocumentOnTypeFormattingOptions& options, const json& json ) {
if ( !json.empty() && json.is_object() ) {
auto ob = json;
options.provider = true;
fromJson( options.triggerCharacters, ob["moreTriggerCharacter"] );
auto trigger = ob["firstTriggerCharacter"].get<std::string>();
if ( trigger.size() )
options.triggerCharacters.push_back( trigger.at( 0 ) );
}
}
static void fromJson( LSPWorkspaceFoldersServerCapabilities& options, const json& json ) {
if ( json.is_object() ) {
auto ob = json;
options.supported = ob["supported"].get<bool>();
auto notify = ob["changeNotifications"].get<bool>();
options.changeNotifications = notify;
}
}
static void fromJson( LSPServerCapabilities& caps, const json& json ) {
// in older protocol versions a support option is simply a boolean
// in newer version it may be an object instead;
// it should not be sent unless such support is announced, but let's handle it anyway
// so consider an object there as a (good?) sign that the server is suitably capable
auto toBoolOrObject = []( const nlohmann::json& value, const std::string& valueName ) {
return value.contains( "valueName" ) &&
( value[valueName].get<bool>() || value[valueName].is_object() );
};
auto sync = json["textDocumentSync"];
caps.textDocumentSync.change = static_cast<LSPDocumentSyncKind>(
( sync.is_object() ? sync["change"].get<int>() : sync.get<bool>() ) );
if ( sync.is_object() ) {
auto syncObject = sync;
auto save = syncObject["save"];
if ( !save.empty() && ( save.is_object() || save.get<bool>() ) ) {
caps.textDocumentSync.save = { save["includeText"].get<bool>() };
}
}
caps.hoverProvider = toBoolOrObject( json, "hoverProvider" );
fromJson( caps.completionProvider, json["completionProvider"] );
fromJson( caps.signatureHelpProvider, json["signatureHelpProvider"] );
caps.definitionProvider = toBoolOrObject( json, "definitionProvider" );
caps.declarationProvider = toBoolOrObject( json, "declarationProvider" );
caps.typeDefinitionProvider = toBoolOrObject( json, "typeDefinitionProvider" );
caps.referencesProvider = toBoolOrObject( json, "referencesProvider" );
caps.implementationProvider = toBoolOrObject( json, "implementationProvider" );
caps.documentSymbolProvider = toBoolOrObject( json, "documentSymbolProvider" );
caps.documentHighlightProvider = toBoolOrObject( json, "documentHighlightProvider" );
caps.documentFormattingProvider = toBoolOrObject( json, "documentFormattingProvider" );
caps.documentRangeFormattingProvider =
toBoolOrObject( json, "documentRangeFormattingProvider" );
fromJson( caps.documentOnTypeFormattingProvider, json["documentOnTypeFormattingProvider"] );
caps.renameProvider = toBoolOrObject( json, "renameProvider" );
if ( json.contains( "codeActionProvider" ) &&
json["codeActionProvider"].contains( "resolveProvider" ) ) {
auto codeActionProvider = json["codeActionProvider"];
caps.codeActionProvider = json["codeActionProvider"]["resolveProvider"].get<bool>();
}
// fromJson( caps.semanticTokenProvider, json["semanticTokensProvider"] );
if ( json.contains( "workspace" ) ) {
auto workspace = json["workspace"];
fromJson( caps.workspaceFolders, workspace["workspaceFolders"] );
}
caps.selectionRangeProvider = toBoolOrObject( json, "selectionRangeProvider" );
}
LSPClientServer::LSPClientServer( LSPClientServerManager* manager, const String::HashType& id,
const LSPDefinition& lsp, const std::string& rootPath ) :
mManager( manager ), mId( id ), mLSP( lsp ), mRootPath( rootPath ) {}
LSPClientServer::~LSPClientServer() {
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.first->unregisterClient( client.second.get() );
{
Lock l( mClientsMutex );
for ( const auto& client : mClients )
client.first->unregisterClient( client.second.get() );
}
}
bool LSPClientServer::start() {
bool ret = mProcess.create(
mLSP.command, Process::getDefaultOptions() | (Uint32)Process::Options::CombinedStdoutStderr,
{}, mRootPath );
bool ret = mProcess.create( mLSP.command, Process::getDefaultOptions(), {}, mRootPath );
if ( ret ) {
mProcess.startAsyncRead( [this]( const char* bytes, size_t n ) { readStdOut( bytes, n ); },
[]( const char*, size_t ) {} );
mProcess.startAsyncRead(
[this]( const char* bytes, size_t n ) { readStdOut( bytes, n ); },
[this]( const char* bytes, size_t n ) { readStdErr( bytes, n ); } );
initialize();
}
@@ -157,10 +322,18 @@ LSPClientServer::RequestHandle LSPClientServer::write( const json& msg,
std::string sjson = ob.dump();
sjson = String::format( "Content-Length: %lu\r\n\r\n%s", sjson.length(), sjson.c_str() );
Log::info( "LSPClient calling %s", msg["method"].get<std::string>().c_str() );
Log::debug( "LSPClient sending message:\n%s", sjson.c_str() );
mProcess.write( sjson );
if ( mReady || msg[MEMBER_METHOD] == "initialize" ) {
std::string method;
if ( msg.contains( MEMBER_METHOD ) )
method = msg[MEMBER_METHOD].get<std::string>();
else if ( msg.contains( MEMBER_MESSAGE ) )
method = msg[MEMBER_MESSAGE];
Log::info( "LSPClientServer calling %s", method.c_str() );
Log::debug( "LSPClientServer sending message:\n%s", sjson.c_str() );
mProcess.write( sjson );
} else {
mQueuedMessages.push_back( { std::move( ob ), h, eh } );
}
return ret;
}
@@ -238,6 +411,10 @@ bool LSPClientServer::hasDocument( TextDocument* doc ) const {
return std::find( mDocs.begin(), mDocs.end(), doc ) != mDocs.end();
}
bool LSPClientServer::hasDocuments() const {
return !mDocs.empty();
}
LSPClientServer::RequestHandle LSPClientServer::didClose( const URI& document ) {
auto params = textDocumentParams( document );
return send( newRequest( "textDocument/didClose", params ) );
@@ -250,9 +427,6 @@ LSPClientServer::RequestHandle LSPClientServer::didClose( TextDocument* doc ) {
auto it = std::find( mDocs.begin(), mDocs.end(), doc );
if ( it != mDocs.end() )
mDocs.erase( it );
// No more docs are being used, close the LSP
if ( mDocs.empty() )
mManager->notifyClose( mId );
}
return ret;
}
@@ -272,7 +446,59 @@ LSPClientServer::RequestHandle LSPClientServer::documentSymbols( const URI& docu
return send( newRequest( "textDocument/documentSymbol", params ), h, eh );
}
/*enum class LSPWorkDoneProgressKind { Begin, Report, End };
struct LSPWorkDoneProgressValue {
LSPWorkDoneProgressKind kind;
std::string title;
std::string message;
bool cancellable;
unsigned percentage;
};
template <typename T> struct LSPProgressParams {
// number or string
json token;
T value;
};
using LSPWorkDoneProgressParams = LSPProgressParams<LSPWorkDoneProgressValue>;
void fromJson( LSPWorkDoneProgressValue& value, const json& json ) {
if ( json ) {
auto ob = json;
auto kind = ob["kind"].get<std::string>();
if ( kind == "begin" ) {
value.kind = LSPWorkDoneProgressKind::Begin;
} else if ( kind == "report" ) {
value.kind = LSPWorkDoneProgressKind::Report;
} else if ( kind == "end" ) {
value.kind = LSPWorkDoneProgressKind::End;
}
value.title = ob["title"].get<std::string>();
value.message = ob["message"].get<std::string>();
value.cancellable = ob["cancellable"].get<bool>();
value.percentage = ob["percentage"].get<int>();
}
}
template <typename T> static LSPProgressParams<T> parseProgress( const json& json ) {
LSPProgressParams<T> ret;
ret.token = json["token"];
fromJson( ret.value, json["value"] );
return ret;
}
static LSPWorkDoneProgressParams parseWorkDone( const json& json ) {
return parseProgress<LSPWorkDoneProgressValue>( json );
}*/
static json newError( const LSPErrorCode& code, const std::string& msg ) {
return json{
{ MEMBER_ERROR, { { MEMBER_CODE, static_cast<int>( code ) }, { MEMBER_MESSAGE, msg } } } };
}
void LSPClientServer::processNotification( const json& msg ) {
Log::warning( "LSPClientServer::processNotification: %s", msg.dump().c_str() );
auto method = msg[MEMBER_METHOD].get<std::string>();
if ( method == "textDocument/publishDiagnostics" ) {
// publishDiagnostics( msg );
@@ -281,61 +507,120 @@ void LSPClientServer::processNotification( const json& msg ) {
} else if ( method == ( "window/logMessage" ) ) {
// logMessage( msg );
} else if ( method == ( "$/progress" ) ) {
// workDoneProgress( msg );
// workDoneProgress( parseWorkDone( msg["params"] ) );
} else {
Log::warning( "LSPClientServer::processNotification msg discarded: %s",
msg.dump( 2, ' ' ) );
}
}
void LSPClientServer::processRequest( const json& /*msg*/ ) {
// auto method = msg[MEMBER_METHOD].get<std::string>();
// auto msgid = msg[MEMBER_ID];
void LSPClientServer::processRequest( const json& msg ) {
Log::debug( "LSPClientServer::processRequest: %s", msg.dump().c_str() );
auto method = msg[MEMBER_METHOD].get<std::string>();
auto msgid = msg[MEMBER_ID].get<int>();
// auto params = msg[MEMBER_PARAMS];
// bool handled = false;
write( newError( LSPErrorCode::MethodNotFound, method ), nullptr, nullptr, msgid );
}
void LSPClientServer::readStdOut( const char* bytes, size_t /*n*/ ) {
const char* skipLength = strstr( bytes, "\r\n\r\n" );
if ( nullptr != skipLength ) {
try {
auto res = json::parse( skipLength + 4 );
Log::debug( "LSP Server %s said: \n%s", mLSP.name.c_str(), res.dump( 2, ' ' ).c_str() );
void LSPClientServer::readStdOut( const char* bytes, size_t n ) {
mReceive.append( bytes, n );
int msgid = -1;
if ( res.contains( MEMBER_ID ) ) {
msgid = res[MEMBER_ID].get<int>();
} else {
processNotification( res );
return;
}
std::string& buffer = mReceive;
if ( res.contains( MEMBER_METHOD ) ) {
processRequest( res );
return;
}
auto it = mHandlers.find( msgid );
if ( it != mHandlers.end() ) {
const auto handler = *it;
mHandlers.erase( it );
auto& h = handler.second.first;
auto& eh = handler.second.second;
if ( res.contains( MEMBER_ERROR ) && eh ) {
eh( res[MEMBER_ERROR] );
} else {
h( res[MEMBER_RESULT] );
}
} else {
Log::debug( "LSPClientServer::readStdOut unexpected reply id: %d", msgid );
}
return;
} catch ( const json::exception& e ) {
Log::debug( "LSP Server %s said: Coudln't parse json err: %s", mLSP.name.c_str(),
e.what() );
while ( true ) {
auto index = buffer.find_first_of( CONTENT_LENGTH_HEADER );
if ( index == std::string::npos ) {
if ( buffer.size() > ( 1 << 20 ) )
buffer.clear();
break;
}
index += strlen( CONTENT_LENGTH_HEADER );
auto endindex = buffer.find_first_of( "\r\n", index );
auto msgstart = buffer.find_first_of( "\r\n\r\n", index );
if ( endindex == std::string::npos || msgstart == std::string::npos )
break;
msgstart += 4;
int length = 0;
bool ok = String::fromString( length, buffer.substr( index, endindex - index ) );
// FIXME perhaps detect if no reply for some time
// then again possibly better left to user to restart in such case
if ( !ok ) {
Log::error( "LSPClientServer::readStdOut invalid " CONTENT_LENGTH );
// flush and try to carry on to some next header
buffer.erase( 0, msgstart );
continue;
}
// sanity check to avoid extensive buffering
if ( length > ( 1 << 29 ) ) {
Log::error( "LSPClientServer::readStdOut excessive size" );
buffer.clear();
continue;
}
if ( msgstart + length > buffer.length() ) {
break;
}
// now onto payload
auto payload = buffer.substr( msgstart, length );
buffer.erase( 0, msgstart + length );
if ( !payload.empty() ) {
try {
auto res = json::parse( payload );
int msgid = -1;
if ( res.contains( MEMBER_ID ) ) {
msgid = res[MEMBER_ID].get<int>();
} else {
processNotification( res );
continue;
}
if ( res.contains( MEMBER_METHOD ) ) {
processRequest( res );
continue;
}
Log::debug( "LSP Server %s said: \n%s", mLSP.name.c_str(), res.dump().c_str() );
auto it = mHandlers.find( msgid );
if ( it != mHandlers.end() ) {
const auto handler = *it;
mHandlers.erase( it );
auto& h = handler.second.first;
auto& eh = handler.second.second;
if ( res.contains( MEMBER_ERROR ) && eh ) {
eh( res[MEMBER_ERROR] );
} else {
h( res[MEMBER_RESULT] );
}
} else {
Log::debug( "LSPClientServer::readStdOut unexpected reply id: %d", msgid );
}
continue;
} catch ( const json::exception& e ) {
Log::debug( "LSP Server %s said: Coudln't parse json err: %s", mLSP.name.c_str(),
e.what() );
}
}
Log::debug( "LSP Server %s said: \n%s", mLSP.name.c_str(), payload.c_str() );
}
}
void LSPClientServer::readStdErr( const char* bytes, size_t n ) {
mReceiveErr += std::string( bytes, n );
LSPShowMessageParams msg;
const auto lastNewLineIndex = mReceiveErr.find_last_of( '\n' );
if ( lastNewLineIndex != std::string::npos ) {
msg.message = mReceiveErr.substr( 0, lastNewLineIndex );
mReceiveErr.erase( 0, lastNewLineIndex + 1 );
}
if ( !msg.message.empty() ) {
msg.type = LSPMessageType::Log;
Log::warning( "LSPClientServer::readStdErr: %s", msg.message.c_str() );
}
Log::debug( "LSP Server %s said: \n%s", mLSP.name.c_str(), bytes );
}
static std::vector<std::string> supportedSemanticTokenTypes() {
@@ -370,62 +655,44 @@ void LSPClientServer::initialize() {
{ "window", json{ { "workDoneProgress", true } } } };
json params{ { "processId", Sys::getProcessID() },
{ "rootPath", !mRootPath.empty() ? mRootPath : "" },
{ "rootUri", !mRootPath.empty() ? "file://" + mRootPath : "" },
{ "capabilities", capabilities },
{ "initializationOptions", {} } };
std::string rootPath = mRootPath;
if ( rootPath.empty() ) {
if ( !mManager->getLSPWorkspaceFolder().uri.empty() )
rootPath = mManager->getLSPWorkspaceFolder().uri.getPath();
else
rootPath = FileSystem::getCurrentWorkingDirectory();
}
std::string uriRootPath = "file://" + rootPath;
params["rootPath"] = rootPath;
params["rootUri"] = uriRootPath;
params["workspaceFolders"] =
toJson( { LSPWorkspaceFolder{ uriRootPath, FileSystem::fileNameFromPath( rootPath ) } } );
capabilities["workspace"] = json{ { "workspaceFolders", true }, { "configuration", false } };
write(
newRequest( "initialize", params ),
[&]( const json& ) {
[&]( const json& resp ) {
try {
fromJson( mCapabilities, resp["capabilities"] );
} catch ( const json::exception& e ) {
Log::warning( "LSPClientServer: error parsing capabilities: %s", e.what() );
}
mReady = true;
write( newRequest( "initialized" ) );
sendQueuedMessages();
},
[&]( const json& ) {
} );
[&]( const json& ) {} );
}
static TextPosition parsePosition( const json& m ) {
auto line = m[MEMBER_LINE].get<int>();
auto column = m[MEMBER_CHARACTER].get<int>();
return { line, column };
}
static TextRange parseRange( const json& range ) {
auto startpos = parsePosition( range[MEMBER_START] );
auto endpos = parsePosition( range[MEMBER_END] );
return { startpos, endpos };
}
static LSPLocation parseLocation( const json& loc ) {
auto uri = URI( loc[MEMBER_URI].get<std::string>() );
auto range = parseRange( loc[MEMBER_RANGE] );
return { uri, range };
}
static LSPLocation parseLocationLink( const json& loc ) {
auto uri = URI( loc[MEMBER_TARGET_URI].get<std::string>() );
auto vrange = loc[MEMBER_TARGET_SELECTION_RANGE];
if ( vrange.is_null() )
vrange = loc[MEMBER_TARGET_RANGE];
auto range = parseRange( vrange );
return { uri, range };
}
static std::vector<LSPLocation> parseDocumentLocation( const json& result ) {
std::vector<LSPLocation> ret;
if ( result.is_array() ) {
const auto& locs = result;
for ( const auto& def : locs ) {
const auto& ob = def;
ret.push_back( parseLocation( ob ) );
if ( ret.back().uri.empty() )
ret.back() = parseLocationLink( ob );
}
} else if ( result.is_object() ) {
ret.push_back( parseLocation( result ) );
}
return ret;
void LSPClientServer::sendQueuedMessages() {
for ( const auto& msg : mQueuedMessages )
write( msg.msg, msg.h, msg.eh );
mQueuedMessages.clear();
}
void LSPClientServer::goToLocation( const json& res ) {
@@ -461,4 +728,11 @@ LSPClientServer::RequestHandle LSPClientServer::documentImplementation( const UR
return getAndGoToLocation( document, pos, "textDocument/implementation" );
}
LSPClientServer::RequestHandle
LSPClientServer::didChangeWorkspaceFolders( const std::vector<LSPWorkspaceFolder>& added,
const std::vector<LSPWorkspaceFolder>& removed ) {
auto params = changeWorkspaceFoldersParams( added, removed );
return send( newRequest( "workspace/didChangeWorkspaceFolders", params ) );
}
} // namespace ecode