#include "lspclientplugin.hpp" #include #include #include #include #include #include #include #include using namespace EE::Window; using json = nlohmann::json; namespace ecode { UICodeEditorPlugin* LSPClientPlugin::New( PluginManager* pluginManager ) { return eeNew( LSPClientPlugin, ( pluginManager ) ); } LSPClientPlugin::LSPClientPlugin( PluginManager* pluginManager ) : mManager( pluginManager ), mThreadPool( pluginManager->getThreadPool() ) { mThreadPool->run( [&, pluginManager] { load( pluginManager ); }, [] {} ); } LSPClientPlugin::~LSPClientPlugin() { mClosing = true; mManager->unsubscribeMessages( this ); Lock l( mDocMutex ); for ( const auto& editor : mEditors ) { for ( auto& kb : mKeyBindings ) { editor.first->getKeyBindings().removeCommandKeybind( kb.first ); if ( editor.first->hasDocument() ) editor.first->getDocument().removeCommand( kb.first ); } editor.first->unregisterPlugin( this ); } if ( nullptr == mManager->getSplitter() ) return; for ( const auto& editor : mEditorsTags ) { if ( mManager->getSplitter()->editorExists( editor.first ) ) { for ( const auto& tag : editor.second ) editor.first->removeActionsByTag( tag ); } } } void LSPClientPlugin::update( UICodeEditor* ) { mClientManager.updateDirty(); } struct LSPPositionAndServer { LSPPosition loc; LSPClientServer* server{ nullptr }; }; LSPClientServer* getServerURIFromURI( LSPClientServerManager& manager, const json& data ) { URI uri( data["uri"] ); return manager.getOneLSPClientServer( uri ); } LSPPositionAndServer getLSPLocationFromJSON( LSPClientServerManager& manager, const json& data ) { if ( !data.contains( "uri" ) || !data.contains( "position" ) ) return {}; TextPosition position( LSPConverter::fromJSON( data["position"] ) ); if ( !position.isValid() ) return {}; URI uri( data["uri"] ); auto server = manager.getOneLSPClientServer( uri ); if ( !server ) return {}; return { { uri, position }, server }; } PluginRequestHandle LSPClientPlugin::processCodeCompletionRequest( const PluginMessage& msg ) { if ( !msg.isRequest() || !msg.isJSON() ) return {}; auto res = getLSPLocationFromJSON( mClientManager, msg.asJSON() ); if ( !res.server ) return {}; auto ret = res.server->documentCompletion( res.loc.uri, res.loc.pos, [&]( const PluginIDType& id, const LSPCompletionList& completionList ) { mManager->sendResponse( this, PluginMessageType::CodeCompletion, PluginMessageFormat::CodeCompletion, &completionList, id ); } ); return std::move( ret ); } PluginRequestHandle LSPClientPlugin::processSignatureHelpRequest( const PluginMessage& msg ) { if ( !msg.isRequest() || !msg.isJSON() ) return {}; auto res = getLSPLocationFromJSON( mClientManager, msg.asJSON() ); if ( !res.server ) return {}; auto ret = res.server->signatureHelp( res.loc.uri, res.loc.pos, [&]( const PluginIDType& id, const LSPSignatureHelp& data ) { mManager->sendResponse( this, PluginMessageType::SignatureHelp, PluginMessageFormat::SignatureHelp, &data, id ); } ); return std::move( ret ); } PluginRequestHandle LSPClientPlugin::processCancelRequest( const PluginMessage& msg ) { if ( !msg.isBroadcast() || !msg.isJSON() ) return {}; auto server = getServerURIFromURI( mClientManager, msg.asJSON() ); if ( !server ) return {}; return server->cancel( LSPClientServer::getID( msg.asJSON() ) ); } PluginRequestHandle LSPClientPlugin::processMessage( const PluginMessage& msg ) { switch ( msg.type ) { case PluginMessageType::WorkspaceFolderChanged: { mClientManager.didChangeWorkspaceFolders( msg.asJSON()["folder"] ); break; } case PluginMessageType::CodeCompletion: { auto ret = processCodeCompletionRequest( msg ); if ( !ret.isEmpty() ) return ret; break; } case PluginMessageType::SignatureHelp: { auto ret = processSignatureHelpRequest( msg ); if ( !ret.isEmpty() ) return ret; break; } case PluginMessageType::LanguageServerCapabilities: { if ( msg.isRequest() && msg.isJSON() ) { const auto& data = msg.asJSON(); if ( !data.contains( "language" ) ) { return {}; } auto server = mClientManager.getOneLSPClientServer( msg.asJSON()["language"] ); if ( server ) { mManager->sendBroadcast( this, PluginMessageType::LanguageServerCapabilities, PluginMessageFormat::LanguageServerCapabilities, &server->getCapabilities() ); return PluginRequestHandle::broadcast(); } } break; } case PluginMessageType::CancelRequest: { processCancelRequest( msg ); break; } default: break; } return PluginRequestHandle::empty(); } void LSPClientPlugin::load( PluginManager* pluginManager ) { pluginManager->subscribeMessages( this, [&]( const auto& notification ) -> PluginRequestHandle { return processMessage( notification ); } ); std::vector paths; std::string path( pluginManager->getResourcesPath() + "plugins/lspclient.json" ); if ( FileSystem::fileExists( path ) ) paths.emplace_back( path ); path = pluginManager->getPluginsPath() + "lspclient.json"; if ( FileSystem::fileExists( path ) || FileSystem::fileWrite( path, "{\n \"config\":{},\n \"keybindings\":{},\n \"servers\":[]\n}\n" ) ) { mConfigPath = path; paths.emplace_back( path ); } if ( paths.empty() ) return; std::vector lsps; for ( const auto& path : paths ) { try { loadLSPConfig( lsps, path, mConfigPath == path ); } catch ( const json::exception& e ) { Log::error( "Parsing LSP \"%s\" failed:\n%s", path.c_str(), e.what() ); } } mClientManager.load( this, pluginManager, std::move( lsps ) ); mReady = mClientManager.lspCount() > 0; for ( const auto& doc : mDelayedDocs ) if ( mDocs.find( doc.first ) != mDocs.end() ) mClientManager.tryRunServer( doc.second ); mDelayedDocs.clear(); if ( mReady ) fireReadyCbs(); } void LSPClientPlugin::loadLSPConfig( std::vector& lsps, const std::string& path, bool updateConfigFile ) { std::string data; if ( !FileSystem::fileGet( path, data ) ) return; json j; try { j = json::parse( data, nullptr, true, true ); } catch ( const json::exception& e ) { Log::error( "LSPClientPlugin::loadLSPConfig - Error parsing LSP config from " "path %s, error: ", path.c_str(), e.what() ); return; } if ( j.contains( "config" ) ) { auto& config = j["config"]; if ( config.contains( "hover_delay" ) ) setHoverDelay( Time::fromString( config["hover_delay"].get() ) ); else if ( updateConfigFile ) config["hover_delay"] = getHoverDelay().toString(); if ( config.contains( "server_close_after_idle_time" ) ) mClientManager.setLSPDecayTime( Time::fromString( config["server_close_after_idle_time"].get() ) ); else if ( updateConfigFile ) config["server_close_after_idle_time"] = mClientManager.getLSPDecayTime().toString(); } if ( mKeyBindings.empty() ) { mKeyBindings["lsp-go-to-definition"] = "f2"; mKeyBindings["lsp-symbol-info"] = "f1"; } if ( j.contains( "keybindings" ) ) { auto& kb = j["keybindings"]; auto list = { "lsp-go-to-definition", "lsp-go-to-declaration", "lsp-go-to-implementation", "lsp-go-to-type-definition", "lsp-switch-header-source", "lsp-symbol-info" }; for ( const auto& key : list ) { if ( kb.contains( key ) ) { if ( !kb[key].empty() ) mKeyBindings[key] = kb[key]; } else if ( updateConfigFile ) kb[key] = mKeyBindings[key]; } } if ( updateConfigFile ) { FileSystem::fileWrite( path, j.dump( 2 ) ); } if ( !j.contains( "servers" ) ) return; auto& servers = j["servers"]; for ( auto& obj : servers ) { // Allow disabling an LSP by redeclaring it in the user configuration file. if ( obj.contains( "name" ) && obj.contains( "disabled" ) && obj.at( "disabled" ).is_boolean() ) { for ( auto& lsp : lsps ) { if ( lsp.name == obj["name"] ) { lsp.disabled = obj["disabled"].get(); break; } } } // Allow setting user command paramaters for an already declared LSP if ( obj.contains( "name" ) && obj.contains( "command_parameters" ) && obj.at( "command_parameters" ).is_string() ) { for ( auto& lsp : lsps ) { if ( lsp.name == obj["name"] ) { lsp.commandParameters = obj.value( "command_parameters", "" ); break; } } } if ( !obj.contains( "language" ) || !obj.contains( "file_patterns" ) ) { Log::warning( "LSP server without language or file_patterns, ignored..." ); continue; } if ( !obj.contains( "use" ) && !( obj.contains( "command" ) && obj.contains( "name" ) ) ) { Log::warning( "LSP server without name+command or use, ignored..." ); continue; } LSPDefinition lsp; if ( obj.contains( "use" ) ) { std::string use = obj["use"]; bool foundTlsp = false; for ( const auto& tlsp : lsps ) { if ( tlsp.name == use ) { lsp.language = obj["language"]; foundTlsp = true; lsp.command = tlsp.command; lsp.name = tlsp.name; lsp.rootIndicationFileNames = tlsp.rootIndicationFileNames; lsp.url = tlsp.url; lsp.initializationOptions = tlsp.initializationOptions; break; } } if ( !foundTlsp ) { Log::warning( "LSP server trying to use an undeclared LSP. Father LSP must be " "declared first." ); continue; } } else { lsp.language = obj["language"]; lsp.command = obj["command"]; lsp.name = obj["name"]; } lsp.url = obj.value( "url", "" ); lsp.commandParameters = obj.value( "command_parameters", lsp.commandParameters ); if ( obj.contains( "initializationOptions" ) ) lsp.initializationOptions = obj["initializationOptions"]; auto fp = obj["file_patterns"]; for ( auto& pattern : fp ) 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 ); } // If the file pattern is repeated, we will overwrite the previous LSP. // The previous LSP should be the "default" LSP that comes with ecode. size_t pos = lspFilePatternPosition( lsps, lsp.filePatterns ); if ( pos != std::string::npos ) { lsps[pos] = lsp; } else { lsps.emplace_back( std::move( lsp ) ); } } } size_t LSPClientPlugin::lspFilePatternPosition( const std::vector& lsps, const std::vector& patterns ) { for ( size_t i = 0; i < lsps.size(); ++i ) { for ( const std::string& filePattern : lsps[i].filePatterns ) { for ( const std::string& pattern : patterns ) { if ( filePattern == pattern ) { return i; } } } } return std::string::npos; } void LSPClientPlugin::onRegister( UICodeEditor* editor ) { Lock l( mDocMutex ); mDocs.insert( editor->getDocumentRef().get() ); for ( auto& kb : mKeyBindings ) { if ( !kb.second.empty() ) editor->getKeyBindings().addKeybindString( kb.second, kb.first ); } if ( editor->hasDocument() ) { editor->getDocument().setCommand( "lsp-go-to-definition", [&, editor]() { mClientManager.getAndGoToLocation( editor->getDocumentRef(), "textDocument/definition" ); } ); editor->getDocument().setCommand( "lsp-go-to-declaration", [&, editor]() { mClientManager.getAndGoToLocation( editor->getDocumentRef(), "textDocument/declaration" ); } ); editor->getDocument().setCommand( "lsp-go-to-implementation", [&, editor]() { mClientManager.getAndGoToLocation( editor->getDocumentRef(), "textDocument/implementation" ); } ); editor->getDocument().setCommand( "lsp-go-to-type-definition", [&, editor]() { mClientManager.getAndGoToLocation( editor->getDocumentRef(), "textDocument/typeDefinition" ); } ); editor->getDocument().setCommand( "lsp-switch-header-source", [&, editor]() { switchSourceHeader( editor ); } ); editor->getDocument().setCommand( "lsp-symbol-info", [&, editor]() { getSymbolInfo( editor ); } ); } std::vector listeners; listeners.push_back( editor->addEventListener( Event::OnDocumentLoaded, [&, editor]( const Event* ) { mClientManager.run( editor->getDocumentRef() ); } ) ); listeners.push_back( editor->addEventListener( Event::OnCursorPosChange, [&, editor]( const Event* ) { if ( mSymbolInfoShowing ) hideTooltip( editor ); } ) ); mEditors.insert( { editor, listeners } ); mEditorsTags.insert( { editor, {} } ); mEditorDocs[editor] = editor->getDocumentRef().get(); if ( mReady && editor->hasDocument() && editor->getDocument().hasFilepath() ) mClientManager.run( editor->getDocumentRef() ); if ( !mReady ) mDelayedDocs[&editor->getDocument()] = editor->getDocumentRef(); } void LSPClientPlugin::switchSourceHeader( UICodeEditor* editor ) { auto* server = mClientManager.getOneLSPClientServer( editor ); if ( server ) server->switchSourceHeader( editor->getDocument().getURI() ); } void LSPClientPlugin::getSymbolInfo( UICodeEditor* editor ) { auto server = mClientManager.getOneLSPClientServer( editor ); if ( server == nullptr ) return; server->documentHover( editor->getDocument().getURI(), editor->getDocument().getSelection().start(), [&, editor]( const Int64&, const LSPHover& resp ) { if ( !resp.contents.empty() && !resp.contents[0].value.empty() ) { editor->runOnMainThread( [editor, resp, this]() { mSymbolInfoShowing = true; displayTooltip( editor, resp, editor->getScreenPosition( editor->getDocument().getSelection().start() ) .getPosition() ); } ); } } ); } void LSPClientPlugin::onUnregister( UICodeEditor* editor ) { for ( auto& kb : mKeyBindings ) { editor->getKeyBindings().removeCommandKeybind( kb.first ); if ( editor->hasDocument() ) editor->getDocument().removeCommand( kb.first ); } if ( mClosing ) return; Lock l( mDocMutex ); TextDocument* doc = mEditorDocs[editor]; auto cbs = mEditors[editor]; for ( auto listener : cbs ) editor->removeEventListener( listener ); mEditors.erase( editor ); mEditorsTags.erase( editor ); mEditorDocs.erase( editor ); for ( auto editor : mEditorDocs ) if ( editor.second == doc ) return; mDocs.erase( doc ); } PluginManager* LSPClientPlugin::getManager() const { return mManager; } bool LSPClientPlugin::onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* menu, const Vector2i& /*position*/, const Uint32& /*flags*/ ) { auto* server = mClientManager.getOneLSPClientServer( editor ); if ( !server ) return false; menu->addSeparator(); auto addFn = [this, editor, menu]( const std::string& txtKey, const std::string& txtVal ) { menu->add( editor->getUISceneNode()->i18n( txtKey, txtVal ), nullptr, KeyBindings::keybindFormat( mKeyBindings[txtKey] ) ) ->setId( txtKey ); }; auto cap = server->getCapabilities(); addFn( "lsp-symbol-info", "Symbol Info" ); if ( cap.definitionProvider ) addFn( "lsp-go-to-definition", "Go To Definition" ); if ( cap.declarationProvider ) addFn( "lsp-go-to-declaration", "Go To Declaration" ); if ( cap.typeDefinitionProvider ) addFn( "lsp-go-to-type-definition", "Go To Type Definition" ); if ( cap.implementationProvider ) addFn( "lsp-go-to-implementation", "Go To Implementation" ); if ( server->getDefinition().language == "cpp" || server->getDefinition().language == "c" ) addFn( "lsp-switch-header-source", "Switch Header/Source" ); return false; } void LSPClientPlugin::hideTooltip( UICodeEditor* editor ) { mSymbolInfoShowing = false; UITooltip* tooltip = nullptr; if ( editor && ( tooltip = editor->getTooltip() ) && tooltip->isVisible() ) { editor->setTooltipText( "" ); tooltip->hide(); // Restore old tooltip state tooltip->setFontStyle( mOldTextStyle ); tooltip->setHorizontalAlign( mOldTextAlign ); tooltip->setUsingCustomStyling( mOldUsingCustomStyling ); tooltip->setDontAutoHideOnMouseMove( mOldDontAutoHideOnMouseMove ); } } TextPosition currentMouseTextPosition( UICodeEditor* editor ) { return editor->resolveScreenPosition( editor->getUISceneNode()->getWindow()->getInput()->getMousePosf() ); } void LSPClientPlugin::tryHideTooltip( UICodeEditor* editor, const Vector2i& position ) { TextPosition cursorPosition = editor->resolveScreenPosition( position.asFloat() ); if ( !mCurrentHover.range.isValid() || ( mCurrentHover.range.isValid() && !mCurrentHover.range.contains( cursorPosition ) ) ) hideTooltip( editor ); } void LSPClientPlugin::displayTooltip( UICodeEditor* editor, const LSPHover& resp, const Vector2f& position ) { editor->setTooltipText( resp.contents[0].value ); // HACK: Gets the old font style to restore it when the tooltip is hidden UITooltip* tooltip = editor->getTooltip(); if ( tooltip == nullptr ) return; mOldTextStyle = tooltip->getFontStyle(); mOldTextAlign = tooltip->getHorizontalAlign(); mOldDontAutoHideOnMouseMove = tooltip->dontAutoHideOnMouseMove(); mOldUsingCustomStyling = tooltip->getUsingCustomStyling(); tooltip->setHorizontalAlign( UI_HALIGN_LEFT ); tooltip->setPixelsPosition( tooltip->getTooltipPosition( position ) ); tooltip->setDontAutoHideOnMouseMove( true ); tooltip->setUsingCustomStyling( true ); tooltip->setFontStyle( Text::Regular ); const auto& syntaxDef = resp.contents[0].kind == LSPMarkupKind::MarkDown ? SyntaxDefinitionManager::instance()->getByLSPName( "markdown" ) : editor->getSyntaxDefinition(); SyntaxTokenizer::tokenizeText( syntaxDef, editor->getColorScheme(), *tooltip->getTextCache(), 0, 0xFFFFFFFF, true, "\n\t " ); tooltip->notifyTextChangedFromTextCache(); if ( editor->hasFocus() && !tooltip->isVisible() ) tooltip->show(); } void LSPClientPlugin::tryDisplayTooltip( UICodeEditor* editor, const LSPHover& resp, const Vector2i& position ) { TextPosition startCursorPosition = editor->resolveScreenPosition( position.asFloat() ); TextPosition currentMousePosition = currentMouseTextPosition( editor ); if ( startCursorPosition != currentMousePosition || !( !resp.contents.empty() && !resp.contents[0].value.empty() && ( ( resp.range.isValid() && resp.range.contains( startCursorPosition ) ) || !resp.range.isValid() ) ) ) return; mCurrentHover = resp; displayTooltip( editor, resp, position.asFloat() ); } bool LSPClientPlugin::onMouseMove( UICodeEditor* editor, const Vector2i& position, const Uint32& flags ) { if ( flags != 0 ) { tryHideTooltip( editor, position ); return false; } String::HashType tag = String::hash( editor->getDocument().getFilePath() ); editor->removeActionsByTag( tag ); mEditorsTags[editor].insert( tag ); editor->runOnMainThread( [&, editor, position, tag]() { mEditorsTags[editor].erase( tag ); mThreadPool->run( [&, editor, position]() { auto server = mClientManager.getOneLSPClientServer( editor ); if ( server == nullptr ) return; server->documentHover( editor->getDocument().getURI(), currentMouseTextPosition( editor ), [&, editor, position]( const Int64&, const LSPHover& resp ) { if ( !resp.contents.empty() && !resp.contents[0].value.empty() ) { editor->runOnMainThread( [editor, resp, position, this]() { tryDisplayTooltip( editor, resp, position ); } ); } } ); } ); }, mHoverDelay, tag ); tryHideTooltip( editor, position ); return editor->getTooltip() && editor->getTooltip()->isVisible(); } void LSPClientPlugin::onFocusLoss( UICodeEditor* editor ) { hideTooltip( editor ); } bool LSPClientPlugin::onKeyDown( UICodeEditor* editor, const KeyEvent& event ) { if ( event.getSanitizedMod() == 0 && event.getKeyCode() == KEY_ESCAPE && editor->getTooltip() && editor->getTooltip()->isVisible() ) { hideTooltip( editor ); } return false; } const Time& LSPClientPlugin::getHoverDelay() const { return mHoverDelay; } void LSPClientPlugin::setHoverDelay( const Time& hoverDelay ) { mHoverDelay = hoverDelay; } const LSPClientServerManager& LSPClientPlugin::getClientManager() const { return mClientManager; } bool LSPClientPlugin::hasFileConfig() { return !mConfigPath.empty(); } std::string LSPClientPlugin::getFileConfigPath() { return mConfigPath; } } // namespace ecode