diff --git a/projects/linux/ee.files b/projects/linux/ee.files index 24cecf98d..c88fac371 100644 --- a/projects/linux/ee.files +++ b/projects/linux/ee.files @@ -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 diff --git a/src/eepp/system/process.cpp b/src/eepp/system/process.cpp index 4604501fe..3002b1543 100644 --- a/src/eepp/system/process.cpp +++ b/src/eepp/system/process.cpp @@ -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 diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 45b76e340..4e4346629 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -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 { diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index ccc3c6dc1..2166a00b8 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -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 #include @@ -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 ); diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp index dc1018b25..ca8c5e40c 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp @@ -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 paths; std::string path( pluginManager->getResourcesPath() + "plugins/lspclient.json" ); if ( FileSystem::fileExists( path ) ) @@ -107,6 +121,8 @@ void LSPClientPlugin::loadLSPConfig( std::vector& 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& lsps, const std lsp.filePatterns.push_back( pattern.get() ); if ( obj.contains( "rootIndicationFileNames" ) ) { + lsp.rootIndicationFileNames.clear(); auto fnms = obj["rootIndicationFileNames"]; for ( auto& fn : fnms ) lsp.rootIndicationFileNames.push_back( fn ); diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp index 6a64788a3..052018979 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp @@ -73,6 +73,8 @@ class LSPClientPlugin : public UICodeEditorPlugin { size_t lspFilePatternPosition( const std::vector& lsps, const std::vector& patterns ); + + void processNotification( PluginManager::Notification, const nlohmann::json& ); }; } // namespace ecode diff --git a/src/tools/ecode/plugins/lsp/lspclientprotocol.hpp b/src/tools/ecode/plugins/lsp/lspclientprotocol.hpp new file mode 100644 index 000000000..e4522bb5a --- /dev/null +++ b/src/tools/ecode/plugins/lsp/lspclientprotocol.hpp @@ -0,0 +1,112 @@ +#ifndef ECODE_LSPCLIENTPROTOCOL_HPP +#define ECODE_LSPCLIENTPROTOCOL_HPP + +#include +#include + +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 triggerCharacters; +}; + +struct LSPSignatureHelpOptions { + bool provider = false; + std::vector 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 diff --git a/src/tools/ecode/plugins/lsp/lspclientserver.cpp b/src/tools/ecode/plugins/lsp/lspclientserver.cpp index 0f7b3042a..228516d52 100644 --- a/src/tools/ecode/plugins/lsp/lspclientserver.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientserver.cpp @@ -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& 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(); + auto column = m[MEMBER_CHARACTER].get(); + 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() ); + auto range = parseRange( loc[MEMBER_RANGE] ); + return { uri, range }; +} + +static LSPLocation parseLocationLink( const json& loc ) { + auto uri = URI( loc[MEMBER_TARGET_URI].get() ); + 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 parseDocumentLocation( const json& result ) { + std::vector 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& added, + const std::vector& 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& trigger, const json& json ) { + if ( !json.empty() ) { + const auto triggersArray = json; + for ( const auto& t : triggersArray ) { + auto st = t.get(); + 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(); + 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(); + 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(); + auto notify = ob["changeNotifications"].get(); + 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() || value[valueName].is_object() ); + }; + + auto sync = json["textDocumentSync"]; + caps.textDocumentSync.change = static_cast( + ( sync.is_object() ? sync["change"].get() : sync.get() ) ); + if ( sync.is_object() ) { + auto syncObject = sync; + auto save = syncObject["save"]; + if ( !save.empty() && ( save.is_object() || save.get() ) ) { + caps.textDocumentSync.save = { save["includeText"].get() }; + } + } + + 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(); + } + // 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().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(); + 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 struct LSPProgressParams { + // number or string + json token; + T value; +}; + +using LSPWorkDoneProgressParams = LSPProgressParams; + +void fromJson( LSPWorkDoneProgressValue& value, const json& json ) { + if ( json ) { + auto ob = json; + auto kind = ob["kind"].get(); + 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(); + value.message = ob["message"].get(); + value.cancellable = ob["cancellable"].get(); + value.percentage = ob["percentage"].get(); + } +} + +template static LSPProgressParams parseProgress( const json& json ) { + LSPProgressParams ret; + ret.token = json["token"]; + fromJson( ret.value, json["value"] ); + return ret; +} + +static LSPWorkDoneProgressParams parseWorkDone( const json& json ) { + return parseProgress( json ); +}*/ + +static json newError( const LSPErrorCode& code, const std::string& msg ) { + return json{ + { MEMBER_ERROR, { { MEMBER_CODE, static_cast( 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(); 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(); - // 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(); + auto msgid = msg[MEMBER_ID].get(); // 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(); - } 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(); + } 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 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(); - auto column = m[MEMBER_CHARACTER].get(); - 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() ); - auto range = parseRange( loc[MEMBER_RANGE] ); - return { uri, range }; -} - -static LSPLocation parseLocationLink( const json& loc ) { - auto uri = URI( loc[MEMBER_TARGET_URI].get() ); - 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 parseDocumentLocation( const json& result ) { - std::vector 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& added, + const std::vector& removed ) { + auto params = changeWorkspaceFoldersParams( added, removed ); + return send( newRequest( "workspace/didChangeWorkspaceFolders", params ) ); +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/lsp/lspclientserver.hpp b/src/tools/ecode/plugins/lsp/lspclientserver.hpp index e3947a46c..d11666b35 100644 --- a/src/tools/ecode/plugins/lsp/lspclientserver.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientserver.hpp @@ -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 @@ -20,11 +21,6 @@ namespace ecode { class LSPClientServerManager; -struct LSPLocation { - URI uri; - TextRange range; -}; - class LSPClientServer { public: template using ReplyHandler = std::function; @@ -101,6 +97,13 @@ class LSPClientServer { void updateDirty(); bool hasDocument( TextDocument* doc ) const; + + bool hasDocuments() const; + + LSPClientServer::RequestHandle + didChangeWorkspaceFolders( const std::vector& added, + const std::vector& removed ); + protected: LSPClientServerManager* mManager{ nullptr }; String::HashType mId; @@ -111,17 +114,31 @@ class LSPClientServer { std::map> mClients; std::map> mHandlers; Mutex mClientsMutex; + bool mReady{ false }; + struct QueueMessage { + json msg; + GenericReplyHandler h; + GenericReplyHandler eh; + }; + std::vector 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 ); diff --git a/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp b/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp index 67713cba3..02b7ba2da 100644 --- a/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp @@ -87,16 +87,21 @@ void LSPClientServerManager::tryRunServer( const std::shared_ptr& } 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& 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 diff --git a/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp b/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp index 248669292..aed5fd75f 100644 --- a/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp @@ -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 mThreadPool; std::map> mClients; std::vector mLSPs; + std::vector mLSPsToClose; + LSPWorkspaceFolder mLSPWorkspaceFolder; std::vector supportsLSP( const std::shared_ptr& doc ); @@ -47,7 +53,7 @@ class LSPClientServerManager { void tryRunServer( const std::shared_ptr& doc ); - void notifyClose( const String::HashType& id ); + void closeLSPServer( const String::HashType& id ); void goToLocation( const LSPLocation& loc ); }; diff --git a/src/tools/ecode/plugins/lsp/lspdocumentclient.cpp b/src/tools/ecode/plugins/lsp/lspdocumentclient.cpp index 4166fafb6..cd4b93354 100644 --- a/src/tools/ecode/plugins/lsp/lspdocumentclient.cpp +++ b/src/tools/ecode/plugins/lsp/lspdocumentclient.cpp @@ -2,6 +2,7 @@ #include "lspclientserver.hpp" #include #include +#include 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* ) {} diff --git a/src/tools/ecode/plugins/lsp/lspdocumentclient.hpp b/src/tools/ecode/plugins/lsp/lspdocumentclient.hpp index 0d39ac405..515d7cad7 100644 --- a/src/tools/ecode/plugins/lsp/lspdocumentclient.hpp +++ b/src/tools/ecode/plugins/lsp/lspdocumentclient.hpp @@ -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& ); diff --git a/src/tools/ecode/plugins/pluginmanager.cpp b/src/tools/ecode/plugins/pluginmanager.cpp index 8ec0bb834..09ee0b79f 100644 --- a/src/tools/ecode/plugins/pluginmanager.cpp +++ b/src/tools/ecode/plugins/pluginmanager.cpp @@ -4,6 +4,8 @@ #include #include +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 cb ) const { + const_cast( this )->mSubscribedPlugins[plugin->getId()] = cb; +} + +void PluginManager::unsubscribeNotifications( UICodeEditorPlugin* plugin ) const { + const_cast( 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(); } diff --git a/src/tools/ecode/plugins/pluginmanager.hpp b/src/tools/ecode/plugins/pluginmanager.hpp index f9dea5949..e03a7f2bb 100644 --- a/src/tools/ecode/plugins/pluginmanager.hpp +++ b/src/tools/ecode/plugins/pluginmanager.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include 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 cb ) const; + + void unsubscribeNotifications( UICodeEditorPlugin* plugin ) const; + protected: friend class App; std::string mResourcesPath; std::string mPluginsPath; + std::string mWorkspaceFolder; std::map mPlugins; std::map mPluginsEnabled; std::map mDefinitions; std::shared_ptr mThreadPool; UICodeEditorSplitter* mSplitter{ nullptr }; + std::map> + mSubscribedPlugins; bool hasDefinition( const std::string& id ); void setSplitter( UICodeEditorSplitter* splitter ); + + void sendNotification( const Notification& notification, const nlohmann::json& json = {} ); }; class PluginsModel : public Model {