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

@@ -1169,6 +1169,7 @@
../../src/tools/ecode/plugins/linter/linterplugin.hpp
../../src/tools/ecode/plugins/lsp/lspclientplugin.cpp
../../src/tools/ecode/plugins/lsp/lspclientplugin.hpp
../../src/tools/ecode/plugins/lsp/lspclientprotocol.hpp
../../src/tools/ecode/plugins/lsp/lspclientserver.cpp
../../src/tools/ecode/plugins/lsp/lspclientserver.hpp
../../src/tools/ecode/plugins/lsp/lspclientservermanager.cpp

View File

@@ -129,7 +129,9 @@ size_t Process::write( const char* buffer, const size_t& size ) {
eeASSERT( mProcess != nullptr );
Lock l( mStdInMutex );
FILE* stdInFile = subprocess_stdin( PROCESS_PTR );
return fwrite( buffer, sizeof( char ), size, stdInFile );
int ret = fwrite( buffer, 1, size, stdInFile );
fflush( stdInFile );
return ret;
}
size_t Process::write( const std::string& buffer ) {
@@ -201,7 +203,7 @@ void Process::startAsyncRead( ReadFn readStdOut, ReadFn readStdErr ) {
}
} );
}
if ( stdErrFd ) {
if ( stdErrFd && stdErrFd != stdOutFd ) {
mStdErrThread = std::thread( [this, stdErrFd]() {
DWORD n;
std::string buffer;
@@ -231,7 +233,7 @@ void Process::startAsyncRead( ReadFn readStdOut, ReadFn readStdErr ) {
: -1;
pollfds.back().events = POLLIN;
}
if ( stdErrFd ) {
if ( stdErrFd && stdOutFd != stdErrFd ) {
pollfds.emplace_back();
pollfds.back().fd =
fcntl( stdErrFd, F_SETFL, fcntl( stdErrFd, F_GETFL ) | O_NONBLOCK ) == 0 ? stdErrFd

View File

@@ -159,6 +159,9 @@ UICodeEditor::UICodeEditor( const bool& autoRegisterBaseCommands,
UICodeEditor( "codeeditor", autoRegisterBaseCommands, autoRegisterBaseKeybindings ) {}
UICodeEditor::~UICodeEditor() {
for ( auto& plugin : mPlugins )
plugin->onUnregister( this );
if ( mDoc.use_count() == 1 ) {
DocEvent event( this, mDoc.get(), Event::OnDocumentClosed );
sendEvent( &event );
@@ -167,8 +170,6 @@ UICodeEditor::~UICodeEditor() {
} else {
mDoc->unregisterClient( this );
}
for ( auto& plugin : mPlugins )
plugin->onUnregister( this );
}
Uint32 UICodeEditor::getType() const {

View File

@@ -2,7 +2,7 @@
#include "plugins/autocomplete/autocompleteplugin.hpp"
#include "plugins/formatter/formatterplugin.hpp"
#include "plugins/linter/linterplugin.hpp"
// #include "plugins/lsp/lspclientplugin.hpp"
#include "plugins/lsp/lspclientplugin.hpp"
#include "version.hpp"
#include <algorithm>
#include <args/args.hxx>
@@ -369,7 +369,7 @@ void App::initPluginManager() {
mPluginManager->registerPlugin( LinterPlugin::Definition() );
mPluginManager->registerPlugin( FormatterPlugin::Definition() );
mPluginManager->registerPlugin( AutoCompletePlugin::Definition() );
// mPluginManager->registerPlugin( LSPClientPlugin::Definition() );
mPluginManager->registerPlugin( LSPClientPlugin::Definition() );
}
void App::loadConfig( const LogLevel& logLevel ) {
@@ -3355,6 +3355,8 @@ void App::loadFolder( const std::string& path ) {
std::string rpath( FileSystem::getRealPath( path ) );
mCurrentProject = rpath;
mPluginManager->setWorkspaceFolder( rpath );
loadDirTree( rpath );
mConfig.loadProject( rpath, mSplitter, mConfigPath, mProjectDocConfig, mThreadPool, this );

View File

@@ -35,7 +35,21 @@ void LSPClientPlugin::update( UICodeEditor* ) {
mClientManager.updateDirty();
}
void LSPClientPlugin::processNotification( PluginManager::Notification noti,
const nlohmann::json& obj ) {
switch ( noti ) {
case PluginManager::WorkspaceFolderChanged: {
mClientManager.didChangeWorkspaceFolders( obj["folder"] );
break;
}
default:
break;
}
}
void LSPClientPlugin::load( const PluginManager* pluginManager ) {
pluginManager->subscribeNotifications(
this, [&]( auto noti, auto obj ) { processNotification( noti, obj ); } );
std::vector<std::string> paths;
std::string path( pluginManager->getResourcesPath() + "plugins/lspclient.json" );
if ( FileSystem::fileExists( path ) )
@@ -107,6 +121,8 @@ void LSPClientPlugin::loadLSPConfig( std::vector<LSPDefinition>& lsps, const std
foundTlsp = true;
lsp.command = tlsp.command;
lsp.name = tlsp.name;
lsp.rootIndicationFileNames = tlsp.rootIndicationFileNames;
lsp.url = tlsp.url;
break;
}
}
@@ -130,6 +146,7 @@ void LSPClientPlugin::loadLSPConfig( std::vector<LSPDefinition>& lsps, const std
lsp.filePatterns.push_back( pattern.get<std::string>() );
if ( obj.contains( "rootIndicationFileNames" ) ) {
lsp.rootIndicationFileNames.clear();
auto fnms = obj["rootIndicationFileNames"];
for ( auto& fn : fnms )
lsp.rootIndicationFileNames.push_back( fn );

View File

@@ -73,6 +73,8 @@ class LSPClientPlugin : public UICodeEditorPlugin {
size_t lspFilePatternPosition( const std::vector<LSPDefinition>& lsps,
const std::vector<std::string>& patterns );
void processNotification( PluginManager::Notification, const nlohmann::json& );
};
} // namespace ecode

View File

@@ -0,0 +1,112 @@
#ifndef ECODE_LSPCLIENTPROTOCOL_HPP
#define ECODE_LSPCLIENTPROTOCOL_HPP
#include <eepp/ui/doc/textdocument.hpp>
#include <string>
using namespace EE::UI::Doc;
using namespace EE::Network;
namespace ecode {
enum class LSPErrorCode {
// Defined by JSON RPC
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
serverErrorStart = -32099,
serverErrorEnd = -32000,
ServerNotInitialized = -32002,
UnknownErrorCode = -32001,
// Defined by the protocol.
RequestCancelled = -32800,
ContentModified = -32801
};
struct LSPLocation {
URI uri;
TextRange range;
};
struct LSPWorkspaceFolder {
URI uri;
std::string name;
};
enum class LSPDocumentSyncKind { None = 0, Full = 1, Incremental = 2 };
struct LSPSaveOptions {
bool includeText = false;
};
struct LSPTextDocumentSyncOptions {
LSPDocumentSyncKind change = LSPDocumentSyncKind::None;
LSPSaveOptions save;
};
struct LSPCompletionOptions {
bool provider = false;
bool resolveProvider = false;
std::vector<char> triggerCharacters;
};
struct LSPSignatureHelpOptions {
bool provider = false;
std::vector<char> triggerCharacters;
};
struct LSPDocumentOnTypeFormattingOptions : public LSPSignatureHelpOptions {};
// Ref:
// https://microsoft.github.io/language-server-protocol/specification#textDocument_semanticTokens
struct LSPSemanticTokensOptions {
bool full = false;
bool fullDelta = false;
bool range = false;
// SemanticTokensLegend legend;
};
struct LSPWorkspaceFoldersServerCapabilities {
bool supported = false;
bool changeNotifications = false;
};
struct LSPServerCapabilities {
LSPTextDocumentSyncOptions textDocumentSync;
bool hoverProvider = false;
LSPCompletionOptions completionProvider;
LSPSignatureHelpOptions signatureHelpProvider;
bool definitionProvider = false;
// official extension as of 3.14.0
bool declarationProvider = false;
bool typeDefinitionProvider = false;
bool referencesProvider = false;
bool implementationProvider = false;
bool documentSymbolProvider = false;
bool documentHighlightProvider = false;
bool documentFormattingProvider = false;
bool documentRangeFormattingProvider = false;
LSPDocumentOnTypeFormattingOptions documentOnTypeFormattingProvider;
bool renameProvider = false;
// CodeActionOptions not useful/considered at present
bool codeActionProvider = false;
LSPSemanticTokensOptions semanticTokenProvider;
// workspace caps flattened
// (other parts not useful/considered at present)
LSPWorkspaceFoldersServerCapabilities workspaceFolders;
bool selectionRangeProvider = false;
};
enum class LSPMessageType { Error = 1, Warning = 2, Info = 3, Log = 4 };
struct LSPShowMessageParams {
LSPMessageType type;
std::string message;
};
} // namespace ecode
#endif // ECODE_LSPCLIENTPROTOCOL_HPP

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

View File

@@ -1,6 +1,7 @@
#ifndef ECODE_LSPCLIENTSERVER_HPP
#ifndef ECODE_LSPCLIENTSERVER_HPP
#define ECODE_LSPCLIENTSERVER_HPP
#include "lspclientprotocol.hpp"
#include "lspdefinition.hpp"
#include "lspdocumentclient.hpp"
#include <eepp/system/process.hpp>
@@ -20,11 +21,6 @@ namespace ecode {
class LSPClientServerManager;
struct LSPLocation {
URI uri;
TextRange range;
};
class LSPClientServer {
public:
template <typename T> using ReplyHandler = std::function<void( const T& )>;
@@ -101,6 +97,13 @@ class LSPClientServer {
void updateDirty();
bool hasDocument( TextDocument* doc ) const;
bool hasDocuments() const;
LSPClientServer::RequestHandle
didChangeWorkspaceFolders( const std::vector<LSPWorkspaceFolder>& added,
const std::vector<LSPWorkspaceFolder>& removed );
protected:
LSPClientServerManager* mManager{ nullptr };
String::HashType mId;
@@ -111,17 +114,31 @@ class LSPClientServer {
std::map<TextDocument*, std::unique_ptr<LSPDocumentClient>> mClients;
std::map<int, std::pair<GenericReplyHandler, GenericReplyHandler>> mHandlers;
Mutex mClientsMutex;
bool mReady{ false };
struct QueueMessage {
json msg;
GenericReplyHandler h;
GenericReplyHandler eh;
};
std::vector<QueueMessage> mQueuedMessages;
std::string mReceive;
std::string mReceiveErr;
LSPServerCapabilities mCapabilities;
int mLastMsgId{ 0 };
int mLastMsgId{ -2147483648 };
void readStdOut( const char* bytes, size_t n );
void readStdErr( const char* bytes, size_t n );
LSPClientServer::RequestHandle write( const json& msg, const GenericReplyHandler& h = nullptr,
const GenericReplyHandler& eh = nullptr,
const int id = 0 );
void initialize();
void sendQueuedMessages();
void processNotification( const json& msg );
void processRequest( const json& msg );

View File

@@ -87,16 +87,21 @@ void LSPClientServerManager::tryRunServer( const std::shared_ptr<TextDocument>&
} else {
server = clientIt->second.get();
}
if ( server )
if ( server ) {
if ( !mLSPWorkspaceFolder.uri.empty() )
server->didChangeWorkspaceFolders( { mLSPWorkspaceFolder }, {} );
server->registerDoc( doc );
}
}
}
void LSPClientServerManager::notifyClose( const String::HashType& id ) {
auto it = mClients.find( id );
if ( it != mClients.end() ) {
mClients.erase( it );
}
void LSPClientServerManager::closeLSPServer( const String::HashType& id ) {
mThreadPool->run( [this, id]() {
auto it = mClients.find( id );
if ( it != mClients.end() ) {
mClients.erase( it );
}
} );
}
void LSPClientServerManager::goToLocation( const LSPLocation& loc ) {
@@ -136,8 +141,16 @@ const std::shared_ptr<ThreadPool>& LSPClientServerManager::getThreadPool() const
}
void LSPClientServerManager::updateDirty() {
for ( auto& server : mClients )
for ( auto& server : mClients ) {
server.second->updateDirty();
if ( !server.second->hasDocuments() )
mLSPsToClose.push_back( server.first );
}
if ( !mLSPsToClose.empty() )
for ( const auto& server : mLSPsToClose )
closeLSPServer( server );
}
void LSPClientServerManager::followSymbolUnderCursor( TextDocument* doc ) {
@@ -149,4 +162,15 @@ void LSPClientServerManager::followSymbolUnderCursor( TextDocument* doc ) {
}
}
void LSPClientServerManager::didChangeWorkspaceFolders( const std::string& folder ) {
mLSPWorkspaceFolder = { "file://" + folder, FileSystem::fileNameFromPath( folder ) };
for ( auto& server : mClients ) {
server.second->didChangeWorkspaceFolders( { mLSPWorkspaceFolder }, {} );
}
}
const LSPWorkspaceFolder& LSPClientServerManager::getLSPWorkspaceFolder() const {
return mLSPWorkspaceFolder;
}
} // namespace ecode

View File

@@ -29,13 +29,19 @@ class LSPClientServerManager {
void followSymbolUnderCursor( TextDocument* doc );
protected:
void didChangeWorkspaceFolders( const std::string& folder );
const LSPWorkspaceFolder& getLSPWorkspaceFolder() const;
protected:
friend class LSPClientServer;
LSPClientPlugin* mPlugin{ nullptr };
std::shared_ptr<ThreadPool> mThreadPool;
std::map<String::HashType, std::unique_ptr<LSPClientServer>> mClients;
std::vector<LSPDefinition> mLSPs;
std::vector<String::HashType> mLSPsToClose;
LSPWorkspaceFolder mLSPWorkspaceFolder;
std::vector<LSPDefinition> supportsLSP( const std::shared_ptr<TextDocument>& doc );
@@ -47,7 +53,7 @@ class LSPClientServerManager {
void tryRunServer( const std::shared_ptr<TextDocument>& doc );
void notifyClose( const String::HashType& id );
void closeLSPServer( const String::HashType& id );
void goToLocation( const LSPLocation& loc );
};

View File

@@ -2,6 +2,7 @@
#include "lspclientserver.hpp"
#include <eepp/system/filesystem.hpp>
#include <eepp/system/iostreamstring.hpp>
#include <eepp/system/log.hpp>
using namespace EE::System;
@@ -12,6 +13,8 @@ LSPDocumentClient::LSPDocumentClient( LSPClientServer* server, TextDocument* doc
notifyOpen();
}
LSPDocumentClient::~LSPDocumentClient() {}
void LSPDocumentClient::onDocumentTextChanged() {
mModified = true;
++mVersion;
@@ -35,7 +38,6 @@ void LSPDocumentClient::onDocumentSaved( TextDocument* ) {
void LSPDocumentClient::onDocumentClosed( TextDocument* ) {
mServer->didClose( mDoc );
mDoc = nullptr;
}
void LSPDocumentClient::onDocumentDirtyOnFileSystem( TextDocument* ) {}

View File

@@ -16,6 +16,8 @@ class LSPDocumentClient : public TextDocument::Client {
public:
LSPDocumentClient( LSPClientServer* server, TextDocument* doc );
~LSPDocumentClient();
virtual void onDocumentTextChanged();
virtual void onDocumentUndoRedo( const TextDocument::UndoRedo& eventType );
virtual void onDocumentCursorChange( const TextPosition& );

View File

@@ -4,6 +4,8 @@
#include <eepp/ui/uitableview.hpp>
#include <eepp/ui/uiwidgetcreator.hpp>
using json = nlohmann::json;
namespace ecode {
PluginManager::PluginManager( const std::string& resourcesPath, const std::string& pluginsPath,
@@ -38,6 +40,7 @@ bool PluginManager::setEnabled( const std::string& id, bool enable ) {
}
if ( !enable && plugin != nullptr ) {
eeSAFE_DELETE( plugin );
mSubscribedPlugins.erase( id );
mPlugins.erase( id );
}
return false;
@@ -95,10 +98,36 @@ UICodeEditorSplitter* PluginManager::getSplitter() const {
return mSplitter;
}
const std::string& PluginManager::getWorkspaceFolder() const {
return mWorkspaceFolder;
}
void PluginManager::setWorkspaceFolder( const std::string& workspaceFolder ) {
mWorkspaceFolder = workspaceFolder;
sendNotification( Notification::WorkspaceFolderChanged,
json{ { "folder", mWorkspaceFolder } } );
}
void PluginManager::subscribeNotifications(
UICodeEditorPlugin* plugin,
std::function<void( Notification, const nlohmann::json& )> cb ) const {
const_cast<PluginManager*>( this )->mSubscribedPlugins[plugin->getId()] = cb;
}
void PluginManager::unsubscribeNotifications( UICodeEditorPlugin* plugin ) const {
const_cast<PluginManager*>( this )->mSubscribedPlugins.erase( plugin->getId() );
}
void PluginManager::setSplitter( UICodeEditorSplitter* splitter ) {
mSplitter = splitter;
}
void PluginManager::sendNotification( const Notification& notification,
const nlohmann::json& json ) {
for ( const auto& plugin : mSubscribedPlugins )
plugin.second( notification, json );
}
bool PluginManager::hasDefinition( const std::string& id ) {
return mDefinitions.find( id ) != mDefinitions.end();
}

View File

@@ -7,6 +7,7 @@
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uiwindow.hpp>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
using namespace EE;
@@ -56,6 +57,8 @@ struct PluginDefinition {
class PluginManager {
public:
enum Notification { WorkspaceFolderChanged };
static constexpr int versionNumber( int major, int minor, int patch ) {
return ( (major)*1000 + (minor)*100 + ( patch ) );
}
@@ -97,19 +100,34 @@ class PluginManager {
UICodeEditorSplitter* getSplitter() const;
const std::string& getWorkspaceFolder() const;
void setWorkspaceFolder( const std::string& workspaceFolder );
void
subscribeNotifications( UICodeEditorPlugin* plugin,
std::function<void( Notification, const nlohmann::json& )> cb ) const;
void unsubscribeNotifications( UICodeEditorPlugin* plugin ) const;
protected:
friend class App;
std::string mResourcesPath;
std::string mPluginsPath;
std::string mWorkspaceFolder;
std::map<std::string, UICodeEditorPlugin*> mPlugins;
std::map<std::string, bool> mPluginsEnabled;
std::map<std::string, PluginDefinition> mDefinitions;
std::shared_ptr<ThreadPool> mThreadPool;
UICodeEditorSplitter* mSplitter{ nullptr };
std::map<std::string, std::function<void( Notification, const nlohmann::json& )>>
mSubscribedPlugins;
bool hasDefinition( const std::string& id );
void setSplitter( UICodeEditorSplitter* splitter );
void sendNotification( const Notification& notification, const nlohmann::json& json = {} );
};
class PluginsModel : public Model {