mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-07-13 22:42:53 +03:00
Fix how `ECODE_SHAREDIR` is set. Fix a very rare race condition in LSPClientPlugin. Always resolve line number when locating files (do not enforce the need of the absolute path). Add aliases for YAML and Markdown.
2103 lines
70 KiB
C++
2103 lines
70 KiB
C++
#include "lspclientplugin.hpp"
|
|
#include "../../version.hpp"
|
|
#include <eepp/graphics/primitives.hpp>
|
|
#include <eepp/system/filesystem.hpp>
|
|
#include <eepp/system/lock.hpp>
|
|
#include <eepp/system/luapattern.hpp>
|
|
#include <eepp/system/scopedop.hpp>
|
|
#include <eepp/ui/tools/uicodeeditorsplitter.hpp>
|
|
#include <eepp/ui/uieventdispatcher.hpp>
|
|
#include <eepp/ui/uilistview.hpp>
|
|
#include <eepp/ui/uiscenenode.hpp>
|
|
#include <eepp/ui/uiscrollbar.hpp>
|
|
#include <eepp/ui/uistyle.hpp>
|
|
#include <eepp/ui/uithememanager.hpp>
|
|
#include <eepp/ui/uitooltip.hpp>
|
|
#include <eepp/ui/uitreeview.hpp>
|
|
#include <eepp/window/engine.hpp>
|
|
#include <eepp/window/input.hpp>
|
|
#include <eepp/window/window.hpp>
|
|
#include <nlohmann/json.hpp>
|
|
#include <utf8cpp/utf8/unchecked.h>
|
|
|
|
using namespace EE::Window;
|
|
using json = nlohmann::json;
|
|
|
|
namespace ecode {
|
|
|
|
static Action::UniqueID getMouseMoveHash( UICodeEditor* editor ) {
|
|
return hashCombine( String::hash( "LSPClientPlugin::onMouseMove-" ),
|
|
reinterpret_cast<Action::UniqueID>( editor ) );
|
|
}
|
|
|
|
static json getURIAndPositionJSON( UICodeEditor* editor ) {
|
|
json data;
|
|
auto doc = editor->getDocumentRef();
|
|
auto sel = doc->getSelection();
|
|
data["uri"] = doc->getURI().toString();
|
|
data["position"] = { { "line", sel.start().line() }, { "character", sel.start().column() } };
|
|
return data;
|
|
}
|
|
|
|
class LSPSymbolInfoTreeModel : public Model {
|
|
public:
|
|
static std::shared_ptr<LSPSymbolInfoTreeModel> create( UISceneNode* uiSceneNode,
|
|
LSPSymbolInformationList&& data,
|
|
const std::string& query = "" ) {
|
|
return std::make_shared<LSPSymbolInfoTreeModel>( uiSceneNode, std::move( data ), query );
|
|
}
|
|
|
|
explicit LSPSymbolInfoTreeModel( UISceneNode* uiSceneNode, LSPSymbolInformationList&& info,
|
|
const std::string& query = "" ) :
|
|
mUISceneNode( uiSceneNode ), mInfo( std::move( info ) ) {
|
|
for ( auto& info : mInfo )
|
|
info.updateParentsRefs();
|
|
if ( !query.empty() )
|
|
filter( String::toLower( query ) );
|
|
}
|
|
|
|
size_t rowCount( const ModelIndex& index ) const override {
|
|
if ( !index.isValid() )
|
|
return mInfo.size();
|
|
if ( index.internalId() == -1 )
|
|
return mInfo[index.row()].children.size();
|
|
if ( index.internalData() )
|
|
return static_cast<LSPSymbolInformation*>( index.internalData() )->children.size();
|
|
return 0;
|
|
};
|
|
|
|
size_t columnCount( const ModelIndex& ) const override { return 1; }
|
|
|
|
ModelIndex index( int row, int column, const ModelIndex& parent ) const override {
|
|
if ( !parent.isValid() )
|
|
return createIndex( row, column, &mInfo[row], -1 );
|
|
if ( parent.internalData() ) {
|
|
LSPSymbolInformation* parentNode =
|
|
static_cast<LSPSymbolInformation*>( parent.internalData() );
|
|
return createIndex( row, column, &parentNode->children[row], parent.row() );
|
|
}
|
|
return {};
|
|
}
|
|
|
|
ModelIndex parentIndex( const ModelIndex& index ) const override {
|
|
if ( !index.isValid() || index.internalId() == -1 )
|
|
return {};
|
|
if ( index.internalData() ) {
|
|
LSPSymbolInformation* node = static_cast<LSPSymbolInformation*>( index.internalData() );
|
|
if ( !node->parent )
|
|
return {};
|
|
return createIndex( index.internalId(), 0, node->parent );
|
|
}
|
|
return {};
|
|
}
|
|
|
|
Variant data( const ModelIndex& index, ModelRole role ) const override {
|
|
LSPSymbolInformation* node = static_cast<LSPSymbolInformation*>( index.internalData() );
|
|
if ( node == nullptr )
|
|
return {};
|
|
|
|
if ( role == ModelRole::Display ) {
|
|
return Variant( node->name.c_str() );
|
|
} else if ( role == ModelRole::Icon ) {
|
|
return mUISceneNode->findIcon( LSPSymbolKindHelper::toIconString( node->kind ) );
|
|
}
|
|
return {};
|
|
}
|
|
|
|
protected:
|
|
UISceneNode* mUISceneNode{ nullptr };
|
|
LSPSymbolInformationList mInfo;
|
|
|
|
std::optional<LSPSymbolInformation> filterNode( const LSPSymbolInformation& node,
|
|
const std::string& query ) {
|
|
LSPSymbolInformation filteredNode( node.name, node.kind, node.range, node.detail );
|
|
|
|
for ( const auto& child : node.children ) {
|
|
if ( auto filteredChild = filterNode( child, query ); filteredChild ) {
|
|
filteredChild->parent = &filteredNode;
|
|
filteredNode.children.push_back( std::move( *filteredChild ) );
|
|
}
|
|
}
|
|
|
|
if ( String::toLower( node.name ).find( query ) != std::string::npos ||
|
|
!filteredNode.children.empty() )
|
|
return filteredNode;
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
void filter( const std::string& query ) {
|
|
std::vector<LSPSymbolInformation> filteredRoots;
|
|
|
|
for ( const auto& rootNode : mInfo )
|
|
if ( auto filteredRoot = filterNode( rootNode, query ); filteredRoot )
|
|
filteredRoots.push_back( std::move( *filteredRoot ) );
|
|
|
|
mInfo = std::move( filteredRoots );
|
|
for ( auto& i : mInfo )
|
|
i.updateParentsRefs();
|
|
}
|
|
};
|
|
|
|
class LSPLocationModel : public Model {
|
|
public:
|
|
enum CustomInfo { URI, TextRange };
|
|
|
|
static std::shared_ptr<LSPLocationModel> create( const std::string& workspaceFolder,
|
|
const std::vector<LSPLocation>& data ) {
|
|
return std::make_shared<LSPLocationModel>( workspaceFolder, data );
|
|
}
|
|
|
|
LSPLocationModel( const std::string& workspaceFolder, const std::vector<LSPLocation>& locs ) {
|
|
createLocs( workspaceFolder, locs );
|
|
}
|
|
|
|
size_t rowCount( const ModelIndex& ) const override { return mLocs.size(); };
|
|
|
|
size_t columnCount( const ModelIndex& ) const override { return 1; }
|
|
|
|
Variant data( const ModelIndex& index, ModelRole role ) const override {
|
|
if ( index.row() >= (Int64)mLocs.size() )
|
|
return {};
|
|
if ( role == ModelRole::Display ) {
|
|
return Variant( mLocs[index.row()].display.c_str() );
|
|
} else if ( role == ModelRole::Custom ) {
|
|
if ( index.column() == CustomInfo::URI ) {
|
|
return Variant( mLocs[index.row()].loc.uri.toString() );
|
|
} else if ( index.column() == CustomInfo::TextRange ) {
|
|
return Variant( mLocs[index.row()].loc.range.toString() );
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
protected:
|
|
struct Location {
|
|
LSPLocation loc;
|
|
std::string display;
|
|
};
|
|
std::vector<Location> mLocs;
|
|
|
|
void createLocs( std::string workspaceFolder, const std::vector<LSPLocation>& locs ) {
|
|
FileSystem::dirAddSlashAtEnd( workspaceFolder );
|
|
|
|
for ( const auto& loc : locs ) {
|
|
std::string display = loc.uri.getFSPath();
|
|
FileSystem::filePathRemoveBasePath( workspaceFolder, display );
|
|
display += " - L" + String::toString( loc.range.start().line() );
|
|
mLocs.push_back( { loc, display } );
|
|
}
|
|
}
|
|
};
|
|
|
|
class LSPCodeActionModel : public Model {
|
|
public:
|
|
static std::shared_ptr<LSPCodeActionModel> create( UISceneNode* sceneNode,
|
|
const std::vector<LSPCodeAction>& data ) {
|
|
return std::make_shared<LSPCodeActionModel>( sceneNode, data );
|
|
}
|
|
|
|
explicit LSPCodeActionModel( UISceneNode* sceneNode, const std::vector<LSPCodeAction>& cas ) :
|
|
mUISceneNode( sceneNode ), mCodeActions( cas ) {}
|
|
|
|
size_t rowCount( const ModelIndex& ) const override {
|
|
return mCodeActions.empty() ? 1 : mCodeActions.size();
|
|
};
|
|
|
|
size_t columnCount( const ModelIndex& ) const override { return 1; }
|
|
|
|
Variant data( const ModelIndex& index, ModelRole role ) const override {
|
|
if ( role == ModelRole::Display ) {
|
|
if ( index.row() < (Int64)mCodeActions.size() ) {
|
|
return Variant( mCodeActions[index.row()].title.c_str() );
|
|
} else {
|
|
return Variant( mUISceneNode->i18n( "no_code_action", "No Code Action" ) );
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
bool hasCodeActions() const { return !mCodeActions.empty(); }
|
|
|
|
const LSPCodeAction& getCodeAction( size_t row ) const { return mCodeActions[row]; }
|
|
|
|
protected:
|
|
UISceneNode* mUISceneNode;
|
|
std::vector<LSPCodeAction> mCodeActions;
|
|
};
|
|
|
|
Plugin* LSPClientPlugin::New( PluginManager* pluginManager ) {
|
|
return eeNew( LSPClientPlugin, ( pluginManager, false ) );
|
|
}
|
|
|
|
Plugin* LSPClientPlugin::NewSync( PluginManager* pluginManager ) {
|
|
return eeNew( LSPClientPlugin, ( pluginManager, true ) );
|
|
}
|
|
|
|
LSPClientPlugin::LSPClientPlugin( PluginManager* pluginManager, bool sync ) :
|
|
Plugin( pluginManager ) {
|
|
if ( sync ) {
|
|
load( pluginManager );
|
|
} else {
|
|
mThreadPool->run( [this, pluginManager] { load( pluginManager ); } );
|
|
}
|
|
}
|
|
|
|
LSPClientPlugin::~LSPClientPlugin() {
|
|
waitUntilLoaded();
|
|
mShuttingDown = true;
|
|
mManager->unsubscribeMessages( this );
|
|
unsubscribeFileSystemListener();
|
|
{
|
|
Lock l( mDocMutex );
|
|
for ( const auto& editor : mEditors ) {
|
|
UICodeEditor* codeEditor = editor.first;
|
|
for ( auto& kb : mKeyBindings ) {
|
|
codeEditor->getKeyBindings().removeCommandKeybind( kb.first );
|
|
if ( codeEditor->hasDocument() )
|
|
codeEditor->getDocument().removeCommand( kb.first );
|
|
}
|
|
for ( auto listener : editor.second )
|
|
codeEditor->removeEventListener( listener );
|
|
if ( mBreadcrumb )
|
|
codeEditor->unregisterTopSpace( this );
|
|
codeEditor->unregisterPlugin( this );
|
|
if ( mManager->getSplitter()->editorExists( codeEditor ) )
|
|
codeEditor->removeActionsByTag( getMouseMoveHash( codeEditor ) );
|
|
}
|
|
if ( nullptr == mManager->getSplitter() )
|
|
return;
|
|
}
|
|
}
|
|
|
|
void LSPClientPlugin::update( UICodeEditor* ) {
|
|
mClientManager.updateDirty();
|
|
}
|
|
|
|
struct LSPPositionAndServer {
|
|
LSPPosition loc;
|
|
LSPClientServer* server{ nullptr };
|
|
};
|
|
|
|
struct LSPURIAndServer {
|
|
URI uri;
|
|
LSPClientServer* server{ nullptr };
|
|
};
|
|
|
|
static LSPClientServer* getServerURIFromURI( LSPClientServerManager& manager, const json& data ) {
|
|
URI uri( data["uri"] );
|
|
return manager.getOneLSPClientServer( uri );
|
|
}
|
|
|
|
static LSPURIAndServer getServerURIFromTextDocumentURI( LSPClientServerManager& manager,
|
|
const json& data ) {
|
|
if ( !data.contains( "textDocument" ) || !data["textDocument"].contains( "uri" ) )
|
|
return {};
|
|
URI uri( data["textDocument"]["uri"] );
|
|
return { uri, manager.getOneLSPClientServer( uri ) };
|
|
}
|
|
|
|
LSPPositionAndServer getLSPLocationFromJSON( LSPClientServerManager& manager, const json& data ) {
|
|
if ( !data.contains( "uri" ) || !data.contains( "position" ) )
|
|
return {};
|
|
|
|
TextPosition position( LSPConverter::parsePosition( 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,
|
|
[this]( const PluginIDType& id, const LSPCompletionList& completionList ) {
|
|
mManager->sendResponse( this, PluginMessageType::CodeCompletion,
|
|
PluginMessageFormat::CodeCompletion, &completionList, id );
|
|
} );
|
|
|
|
return 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, [this]( const PluginIDType& id, const LSPSignatureHelp& data ) {
|
|
mManager->sendResponse( this, PluginMessageType::SignatureHelp,
|
|
PluginMessageFormat::SignatureHelp, &data, id );
|
|
} );
|
|
|
|
return ret;
|
|
}
|
|
|
|
PluginRequestHandle LSPClientPlugin::processDocumentFormatting( const PluginMessage& msg ) {
|
|
if ( !msg.isBroadcast() || !msg.isJSON() )
|
|
return {};
|
|
|
|
auto server = getServerURIFromTextDocumentURI( mClientManager, msg.asJSON() );
|
|
if ( !server.server )
|
|
return {};
|
|
|
|
if ( !msg.asJSON().contains( "options" ) )
|
|
return {};
|
|
|
|
auto ret = server.server->documentFormatting(
|
|
server.uri, msg.asJSON()["options"],
|
|
[this, server]( const PluginIDType&, const std::vector<LSPTextEdit>& edits ) {
|
|
mManager->getSplitter()->getUISceneNode()->runOnMainThread(
|
|
[this, server, edits] { processDocumentFormattingResponse( server.uri, edits ); } );
|
|
} );
|
|
|
|
return ret;
|
|
}
|
|
|
|
PluginRequestHandle LSPClientPlugin::processWorkspaceSymbol( const PluginMessage& msg ) {
|
|
if ( !msg.isRequest() || !msg.isJSON() )
|
|
return {};
|
|
|
|
auto servers = mClientManager.getFilteredServers( []( LSPClientServer* server ) {
|
|
return server->getCapabilities().workspaceSymbolProvider;
|
|
} );
|
|
if ( servers.empty() )
|
|
return {};
|
|
|
|
json jmsg = msg.asJSON();
|
|
std::string query = jmsg.value( "query", "" );
|
|
PluginRequestHandle hdl;
|
|
|
|
for ( const auto server : servers ) {
|
|
if ( Engine::instance()->isMainThread() ) {
|
|
server->workspaceSymbolAsync(
|
|
query, [this, query]( const PluginIDType& id, LSPSymbolInformationList&& info ) {
|
|
if ( !query.empty() ) {
|
|
for ( auto& i : info ) {
|
|
if ( i.score == 0.f )
|
|
i.score = String::fuzzyMatch( query, i.name );
|
|
}
|
|
}
|
|
mManager->sendResponse( this, PluginMessageType::WorkspaceSymbol,
|
|
PluginMessageFormat::SymbolInformation, &info, id );
|
|
} );
|
|
} else {
|
|
hdl = server->workspaceSymbol(
|
|
query, [this, query]( const PluginIDType& id, LSPSymbolInformationList&& info ) {
|
|
if ( !query.empty() ) {
|
|
for ( auto& i : info ) {
|
|
if ( i.score == 0.f )
|
|
i.score = String::fuzzyMatch( query, i.name );
|
|
}
|
|
}
|
|
mManager->sendResponse( this, PluginMessageType::WorkspaceSymbol,
|
|
PluginMessageFormat::SymbolInformation, &info, id );
|
|
} );
|
|
}
|
|
}
|
|
|
|
return hdl;
|
|
}
|
|
|
|
PluginRequestHandle LSPClientPlugin::processWorkspaceDiagnostic( const PluginMessage& msg ) {
|
|
if ( !( ( msg.isBroadcast() && msg.type == PluginMessageType::LanguageServerReady && msg.data &&
|
|
msg.format == PluginMessageFormat::LSPClientServer ) ||
|
|
( msg.isRequest() && msg.type == PluginMessageType::WorkspaceDiagnostic &&
|
|
msg.format == PluginMessageFormat::LSPClientServer ) ) )
|
|
return {};
|
|
/* NOTE: I couldn't find a single LSP server supporting this feature so I cannot test it.
|
|
* I'll leave the current implementation here to continue with it later in the future.
|
|
* Since I started implementing it assuming it was commonly supported...
|
|
|
|
LSPClientServer* server =
|
|
const_cast<LSPClientServer*>( reinterpret_cast<const LSPClientServer*>( msg.data ) );
|
|
|
|
if ( !server->getCapabilities().diagnosticProvider.workspaceDiagnostics )
|
|
return {};
|
|
|
|
server->workspaceDiagnosticAsync(
|
|
[this]( const PluginIDType&, LSPWorkspaceDiagnosticReport&& report ) {
|
|
mManager->sendBroadcast( this, PluginMessageType::WorkspaceDiagnostic,
|
|
PluginMessageFormat::WorkspaceDiagnosticReport, &report );
|
|
} );
|
|
*/
|
|
return PluginRequestHandle::broadcast();
|
|
}
|
|
|
|
PluginRequestHandle LSPClientPlugin::processTextDocumentSymbol( const PluginMessage& msg ) {
|
|
if ( !msg.isRequest() ||
|
|
( msg.type != PluginMessageType::TextDocumentSymbol &&
|
|
msg.type != PluginMessageType::TextDocumentFlattenSymbol ) ||
|
|
msg.format != PluginMessageFormat::JSON || !msg.asJSON().contains( "uri" ) )
|
|
return {};
|
|
|
|
URI uri( msg.asJSON().value( "uri", "" ) );
|
|
if ( uri.empty() )
|
|
return {};
|
|
|
|
const LSPSymbolInformationList& symbols = msg.type == PluginMessageType::TextDocumentSymbol
|
|
? getDocumentSymbols( uri )
|
|
: getDocumentFlattenSymbols( uri );
|
|
{
|
|
Lock l( mDocSymbolsMutex );
|
|
if ( !symbols.empty() ) {
|
|
mManager->sendResponse( this, msg.type, PluginMessageFormat::SymbolInformation,
|
|
&symbols, uri.toString() );
|
|
return { uri.toString() };
|
|
}
|
|
}
|
|
|
|
LSPClientServer* server = mClientManager.getOneLSPClientServer( uri );
|
|
if ( !server || !server->getCapabilities().documentSymbolProvider )
|
|
return {};
|
|
auto handler = [uri, this]( const PluginIDType& id, LSPSymbolInformationList&& res ) {
|
|
setDocumentSymbolsFromResponse( id, uri, std::move( res ) );
|
|
};
|
|
|
|
server->documentSymbols( uri, handler );
|
|
|
|
return { uri.toString() };
|
|
}
|
|
|
|
PluginRequestHandle LSPClientPlugin::processFoldingRanges( const PluginMessage& msg ) {
|
|
if ( !msg.isRequest() || msg.type != PluginMessageType::FoldingRanges ||
|
|
msg.format != PluginMessageFormat::JSON || !msg.asJSON().contains( "uri" ) )
|
|
return {};
|
|
|
|
URI uri( msg.asJSON().value( "uri", "" ) );
|
|
if ( uri.empty() )
|
|
return {};
|
|
|
|
LSPClientServer* server = mClientManager.getOneLSPClientServer( uri );
|
|
if ( !server || !server->getCapabilities().foldingRangeProvider )
|
|
return {};
|
|
|
|
auto handler = [uri, this]( const PluginIDType& id, const std::vector<LSPFoldingRange>& res ) {
|
|
mManager->sendResponse( this, PluginMessageType::FoldingRanges,
|
|
PluginMessageFormat::FoldingRanges, &res, id );
|
|
};
|
|
|
|
if ( Engine::instance()->isMainThread() ) {
|
|
server->getThreadPool()->run(
|
|
[server, uri, handler]() { server->documentFoldingRange( uri, handler ); } );
|
|
} else {
|
|
server->documentFoldingRange( uri, handler );
|
|
}
|
|
|
|
return { uri.toString() };
|
|
}
|
|
|
|
void processFormattingResponse( const std::shared_ptr<TextDocument>& doc,
|
|
std::vector<LSPTextEdit> edits ) {
|
|
TextRanges ranges = doc->getSelections();
|
|
|
|
doc->resetSelection();
|
|
doc->setRunningTransaction( true );
|
|
|
|
// Sort from bottom to top, this way we don't have to compute any position deltas
|
|
std::sort( edits.begin(), edits.end(), []( const LSPTextEdit& left, const LSPTextEdit& right ) {
|
|
return left.range > right.range;
|
|
} );
|
|
|
|
for ( const auto& edit : edits ) {
|
|
doc->setSelection( edit.range );
|
|
if ( edit.text.empty() ) {
|
|
doc->deleteSelection( 0 );
|
|
} else {
|
|
if ( edit.range.hasSelection() )
|
|
doc->deleteTo( 0, 0 );
|
|
doc->insert( 0, doc->getSelectionIndex( 0 ).start(), edit.text );
|
|
}
|
|
}
|
|
|
|
doc->setSelection( ranges );
|
|
|
|
doc->setRunningTransaction( false );
|
|
}
|
|
|
|
bool LSPClientPlugin::processDocumentFormattingResponse( const URI& uri,
|
|
std::vector<LSPTextEdit> edits ) {
|
|
for ( const auto& edit : edits )
|
|
if ( !edit.range.isValid() )
|
|
return false;
|
|
|
|
auto doc = mManager->getSplitter()->findDocFromURI( uri );
|
|
if ( !doc ) {
|
|
mManager->getLoadFileFn()(
|
|
uri.getFSPath(), [this, edits = std::move( edits )]( UICodeEditor* editor, auto ) {
|
|
auto documentRef = editor->getDocumentRef();
|
|
mManager->getSplitter()->getUISceneNode()->runOnMainThread(
|
|
[documentRef, edits]() { processFormattingResponse( documentRef, edits ); } );
|
|
} );
|
|
return false;
|
|
}
|
|
|
|
processFormattingResponse( doc, edits );
|
|
|
|
return true;
|
|
}
|
|
|
|
void LSPClientPlugin::setDocumentSymbols( const URI& docURI, LSPSymbolInformationList&& res ) {
|
|
Lock l( mDocSymbolsMutex );
|
|
mDocFlatSymbols[docURI] = !LSPSymbolInformationListHelper::isFlat( res )
|
|
? LSPSymbolInformationListHelper::flatten( res )
|
|
: res;
|
|
|
|
for ( auto& r : res )
|
|
r.updateParentsRefs();
|
|
|
|
mDocSymbols[docURI] = std::move( res );
|
|
|
|
getManager()->getSplitter()->forEachDoc( [docURI, this]( TextDocument& doc ) {
|
|
if ( doc.getURI() == docURI )
|
|
updateCurrentSymbol( doc );
|
|
} );
|
|
}
|
|
|
|
void LSPClientPlugin::setDocumentSymbolsFromResponse( const PluginIDType& id, const URI& docURI,
|
|
LSPSymbolInformationList&& res ) {
|
|
setDocumentSymbols( docURI, std::move( res ) );
|
|
|
|
{
|
|
const auto& docSymbols = getDocumentSymbols( docURI );
|
|
Lock l( mDocSymbolsMutex );
|
|
mManager->sendResponse( this, PluginMessageType::TextDocumentSymbol,
|
|
PluginMessageFormat::SymbolInformation, &docSymbols, id );
|
|
}
|
|
|
|
{
|
|
const auto& docFlattenSymbols = getDocumentFlattenSymbols( docURI );
|
|
Lock l( mDocSymbolsMutex );
|
|
mManager->sendResponse( this, PluginMessageType::TextDocumentFlattenSymbol,
|
|
PluginMessageFormat::SymbolInformation, &docFlattenSymbols, id );
|
|
}
|
|
}
|
|
|
|
const LSPSymbolInformationList& LSPClientPlugin::getDocumentSymbols( const URI& docURI ) {
|
|
Lock l( mDocSymbolsMutex );
|
|
auto foundIt = mDocSymbols.find( docURI );
|
|
if ( foundIt != mDocSymbols.end() )
|
|
return foundIt->second;
|
|
mDocSymbols[docURI] = {};
|
|
return mDocSymbols[docURI];
|
|
}
|
|
|
|
const LSPSymbolInformationList& LSPClientPlugin::getDocumentFlattenSymbols( const URI& docURI ) {
|
|
Lock l( mDocSymbolsMutex );
|
|
auto foundIt = mDocFlatSymbols.find( docURI );
|
|
if ( foundIt != mDocFlatSymbols.end() )
|
|
return foundIt->second;
|
|
mDocFlatSymbols[docURI] = {};
|
|
return mDocFlatSymbols[docURI];
|
|
}
|
|
|
|
TextDocument* LSPClientPlugin::getDocumentFromURI( const URI& uri ) {
|
|
Lock l( mDocMutex );
|
|
for ( const auto client : mDocs )
|
|
if ( client->getURI() == uri )
|
|
return client;
|
|
return nullptr;
|
|
}
|
|
|
|
bool LSPClientPlugin::onMouseClick( UICodeEditor* editor, const Vector2i& pos,
|
|
const Uint32& flags ) {
|
|
const bool breadcrumbClick = mBreadcrumb && ( flags & EE_BUTTON_LMASK );
|
|
const Vector2f localPos( breadcrumbClick ? editor->convertToNodeSpace( pos.asFloat() )
|
|
: Vector2f::Zero );
|
|
if ( breadcrumbClick && localPos.y >= 0 && localPos.y < mPluginTopSpace &&
|
|
localPos.x < editor->getTopAreaWidth() ) {
|
|
const Vector2f downLocalPos( editor->convertToNodeSpace(
|
|
editor->getEventDispatcher()->getMouseDownPos().asFloat() ) );
|
|
if ( downLocalPos.y >= 0 && downLocalPos.y < mPluginTopSpace &&
|
|
downLocalPos.x < editor->getTopAreaWidth() ) {
|
|
editor->getDocument().execute( "lsp-show-document-symbols", editor );
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Input* input = editor->getInput();
|
|
Uint32 mod = input->getSanitizedModState();
|
|
if ( mod != ( KEYMOD_LALT | KeyMod::getDefaultModifier() ) || ( flags & EE_BUTTON_LMASK ) == 0 )
|
|
return false;
|
|
|
|
auto docPos = editor->resolveScreenPosition( pos.asFloat() );
|
|
if ( !docPos.isValid() || !editor->getDocument().isValidPosition( docPos ) )
|
|
return false;
|
|
|
|
editor->getDocument().execute( "lsp-go-to-definition", editor );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool LSPClientPlugin::semanticHighlightingEnabled() const {
|
|
return mSemanticHighlighting;
|
|
}
|
|
|
|
void LSPClientPlugin::setSemanticHighlighting( bool semanticHighlighting ) {
|
|
mSemanticHighlighting = semanticHighlighting;
|
|
}
|
|
|
|
bool LSPClientPlugin::langSupportsSemanticHighlighting( const std::string& lspLang ) {
|
|
return !std::any_of( mSemanticHighlightingDisabledLangs.begin(),
|
|
mSemanticHighlightingDisabledLangs.end(),
|
|
[&lspLang]( const auto& other ) { return lspLang == other; } );
|
|
}
|
|
|
|
bool LSPClientPlugin::isSilent() const {
|
|
return mSilence;
|
|
}
|
|
|
|
void LSPClientPlugin::setSilent( bool silence ) {
|
|
mSilence = silence;
|
|
}
|
|
|
|
bool LSPClientPlugin::trimLogs() const {
|
|
return mTrimLogs;
|
|
}
|
|
|
|
void LSPClientPlugin::setTrimLogs( bool trimLogs ) {
|
|
mTrimLogs = trimLogs;
|
|
}
|
|
|
|
void LSPClientPlugin::createLocationsView( UICodeEditor* editor,
|
|
const std::vector<LSPLocation>& res ) {
|
|
auto model = LSPLocationModel::create( mManager->getWorkspaceFolder(), res );
|
|
createListView( editor, model, [this]( const ModelEvent* modelEvent ) {
|
|
if ( modelEvent->getModelEventType() != ModelEventType::Open )
|
|
return;
|
|
auto r = modelEvent->getModelIndex().row();
|
|
Variant uri( modelEvent->getModel()->data(
|
|
modelEvent->getModel()->index( r, LSPLocationModel::CustomInfo::URI ),
|
|
ModelRole::Custom ) );
|
|
Variant range( modelEvent->getModel()->data(
|
|
modelEvent->getModel()->index( r, LSPLocationModel::CustomInfo::TextRange ),
|
|
ModelRole::Custom ) );
|
|
LSPLocation loc;
|
|
loc.uri = URI( uri.asStdString() );
|
|
loc.range = TextRange::fromString( range.asStdString() );
|
|
mClientManager.goToLocation( loc );
|
|
modelEvent->getNode()->close();
|
|
} );
|
|
}
|
|
|
|
void LSPClientPlugin::createCodeActionsView( UICodeEditor* editor,
|
|
const std::vector<LSPCodeAction>& cas ) {
|
|
bool casEmpty = cas.empty();
|
|
std::shared_ptr<LSPCodeActionModel> model;
|
|
if ( mQuickFix.title.empty() && mQuickFix.kind.empty() &&
|
|
mQuickFix.edit.documentChanges.empty() && mQuickFix.edit.changes.empty() ) {
|
|
model = LSPCodeActionModel::create( mManager->getSplitter()->getUISceneNode(), cas );
|
|
} else {
|
|
casEmpty = false;
|
|
auto casN( cas );
|
|
bool foundRepeated = false;
|
|
for ( const auto& c : casN ) {
|
|
if ( mQuickFix.title == c.title ) {
|
|
foundRepeated = true;
|
|
break;
|
|
}
|
|
}
|
|
if ( !foundRepeated ) {
|
|
LSPCodeAction action{ mQuickFix.title, mQuickFix.kind, {}, mQuickFix.edit, {},
|
|
mQuickFix.isPreferred };
|
|
casN.insert( casN.begin(), action );
|
|
}
|
|
model = LSPCodeActionModel::create( mManager->getSplitter()->getUISceneNode(), casN );
|
|
}
|
|
|
|
createListView(
|
|
editor, model,
|
|
[this, editor]( const ModelEvent* modelEvent ) {
|
|
if ( modelEvent->getModelEventType() != ModelEventType::Open )
|
|
return;
|
|
auto r = modelEvent->getModelIndex().row();
|
|
const auto cam = static_cast<const LSPCodeActionModel*>( modelEvent->getModel() );
|
|
if ( cam->hasCodeActions() && editorExists( editor ) ) {
|
|
const auto& ca = cam->getCodeAction( r );
|
|
|
|
if ( !ca.edit.changes.empty() || !ca.edit.documentChanges.empty() )
|
|
mClientManager.applyWorkspaceEdit( ca.edit, []( const auto& ) {} );
|
|
|
|
auto server = mClientManager.getOneLSPClientServer( editor->getDocumentRef() );
|
|
if ( server && server->getCapabilities().executeCommandProvider )
|
|
mClientManager.executeCommand( editor->getDocumentRef(), ca.command );
|
|
editor->setFocus();
|
|
} else if ( editorExists( editor ) ) {
|
|
editor->setFocus();
|
|
}
|
|
modelEvent->getNode()->close();
|
|
},
|
|
[this, casEmpty, editor]( UIListView* lv ) {
|
|
if ( casEmpty ) {
|
|
lv->runOnMainThread(
|
|
[this, editor] {
|
|
if ( editorExists( editor ) &&
|
|
getManager()->getSplitter()->isCurEditor( editor ) ) {
|
|
editor->setFocus();
|
|
}
|
|
},
|
|
Seconds( 1 ) );
|
|
}
|
|
} );
|
|
}
|
|
|
|
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::FileSystemListenerReady: {
|
|
subscribeFileSystemListener();
|
|
break;
|
|
}
|
|
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::DocumentFormatting: {
|
|
auto ret = processDocumentFormatting( msg );
|
|
if ( !ret.isEmpty() )
|
|
return ret;
|
|
break;
|
|
}
|
|
case PluginMessageType::QueryPluginCapability: {
|
|
if ( msg.isRequest() && msg.isJSON() ) {
|
|
const auto& data = msg.asJSON();
|
|
if ( !data.contains( "capability" ) || !data["capability"].is_number_integer() ||
|
|
data["capability"].get<int>() < 0 ||
|
|
data["capability"].get<int>() >= static_cast<int>( PluginCapability::Max ) )
|
|
return {};
|
|
PluginCapability capability = data["capability"].get<PluginCapability>();
|
|
URI uri;
|
|
if ( data.contains( "uri" ) && data["uri"].is_string() )
|
|
uri = data["uri"];
|
|
PluginImmediateResponse rmsg;
|
|
rmsg.type = PluginMessageType::QueryPluginCapability;
|
|
auto servers = mClientManager.getFilteredServers(
|
|
[capability, uri]( LSPClientServer* server ) {
|
|
if ( !uri.empty() && !server->hasDocument( uri ) )
|
|
return false;
|
|
switch ( capability ) {
|
|
case PluginCapability::WorkspaceSymbol:
|
|
return server->getCapabilities().workspaceSymbolProvider;
|
|
case PluginCapability::TextDocumentSymbol:
|
|
return server->getCapabilities().documentSymbolProvider;
|
|
case PluginCapability::FoldingRange:
|
|
return server->getCapabilities().foldingRangeProvider;
|
|
default: {
|
|
}
|
|
}
|
|
return false;
|
|
} );
|
|
rmsg.data = !servers.empty();
|
|
return PluginRequestHandle( rmsg );
|
|
}
|
|
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;
|
|
}
|
|
case PluginMessageType::WorkspaceSymbol: {
|
|
auto ret = processWorkspaceSymbol( msg );
|
|
if ( !ret.isEmpty() )
|
|
return ret;
|
|
break;
|
|
}
|
|
case PluginMessageType::TextDocumentSymbol:
|
|
case PluginMessageType::TextDocumentFlattenSymbol: {
|
|
processTextDocumentSymbol( msg );
|
|
break;
|
|
}
|
|
case PluginMessageType::DiagnosticsCodeAction: {
|
|
processDiagnosticsCodeAction( msg );
|
|
break;
|
|
}
|
|
case PluginMessageType::FoldingRanges: {
|
|
processFoldingRanges( msg );
|
|
break;
|
|
}
|
|
case PluginMessageType::WorkspaceDiagnostic:
|
|
case PluginMessageType::LanguageServerReady: {
|
|
processWorkspaceDiagnostic( msg );
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
return PluginRequestHandle::empty();
|
|
}
|
|
|
|
void LSPClientPlugin::load( PluginManager* pluginManager ) {
|
|
Clock clock;
|
|
AtomicBoolScopedOp loading( mLoading, true );
|
|
pluginManager->subscribeMessages( this,
|
|
[this]( const auto& notification ) -> PluginRequestHandle {
|
|
return processMessage( notification );
|
|
} );
|
|
std::vector<std::string> 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<LSPDefinition> lsps;
|
|
|
|
for ( const auto& ipath : paths ) {
|
|
try {
|
|
loadLSPConfig( lsps, ipath, mConfigPath == ipath );
|
|
} catch ( const json::exception& e ) {
|
|
Log::error( "Parsing LSP \"%s\" failed:\n%s", ipath.c_str(), e.what() );
|
|
}
|
|
}
|
|
|
|
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> lspPatternsLangId;
|
|
|
|
for ( auto& lsp : lsps ) {
|
|
if ( lsp.shareProcessWithOtherDefinition && !lsp.usesLSP.empty() ) {
|
|
for ( const auto& ptrn : lsp.filePatterns )
|
|
lspPatternsLangId[lsp.usesLSP][ptrn] = lsp.language;
|
|
}
|
|
}
|
|
|
|
for ( auto& lsp : lsps ) {
|
|
auto ptrnsLangIds = lspPatternsLangId.find( lsp.name );
|
|
if ( ptrnsLangIds != lspPatternsLangId.end() ) {
|
|
lsp.languageIdsForFilePatterns.merge( ptrnsLangIds->second );
|
|
for ( const auto& ptrn : lsp.filePatterns )
|
|
lsp.languageIdsForFilePatterns[ptrn] = lsp.language;
|
|
}
|
|
}
|
|
|
|
mClientManager.load( this, pluginManager, std::move( lsps ) );
|
|
|
|
subscribeFileSystemListener();
|
|
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();
|
|
setReady( clock.getElapsedTime() );
|
|
}
|
|
}
|
|
|
|
static std::string parseCommand( nlohmann::json cmd, const std::string& workspaceFolder ) {
|
|
std::string command;
|
|
if ( cmd.is_string() ) {
|
|
command = cmd.get<std::string>();
|
|
} else if ( !cmd.is_object() ) {
|
|
return "";
|
|
} else {
|
|
std::string platform( String::toLower( Sys::getPlatform() ) );
|
|
if ( cmd.contains( platform ) && cmd[platform].is_string() ) {
|
|
command = cmd[platform].get<std::string>();
|
|
} else {
|
|
platform = "other";
|
|
if ( cmd.contains( platform ) && cmd[platform].is_string() )
|
|
command = cmd[platform].get<std::string>();
|
|
}
|
|
}
|
|
LSPClientServer::sanitizeCommand( command, workspaceFolder );
|
|
return command;
|
|
}
|
|
|
|
static void tryAddEnv( const json& obj, LSPDefinition& lsp ) {
|
|
if ( obj.contains( "env" ) && obj.is_array() ) {
|
|
for ( const auto& obje : obj ) {
|
|
if ( !obje.is_string() )
|
|
continue;
|
|
std::string envStr = obje.get<std::string>();
|
|
if ( !envStr.empty() && envStr.find_first_of( "=" ) != std::string::npos ) {
|
|
auto envp = String::split( envStr, '=' );
|
|
if ( envp.size() == 2 ) {
|
|
lsp.env[envp[0]] = envp[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LSPClientPlugin::loadLSPConfig( std::vector<LSPDefinition>& 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: %s, config file content:\n%s",
|
|
path.c_str(), e.what(), data.c_str() );
|
|
if ( !updateConfigFile )
|
|
return;
|
|
// Recreate it
|
|
j = json::parse( "{\n \"config\":{},\n \"keybindings\":{},\n \"servers\":[]\n}\n",
|
|
nullptr, true, true );
|
|
}
|
|
|
|
if ( updateConfigFile ) {
|
|
mConfigHash = String::hash( data );
|
|
}
|
|
|
|
if ( j.contains( "config" ) ) {
|
|
auto& config = j["config"];
|
|
|
|
if ( config.contains( "silent" ) )
|
|
mSilence = config.value( "silent", false );
|
|
else if ( updateConfigFile )
|
|
config["silent"] = mSilence;
|
|
|
|
if ( config.contains( "trim_logs" ) )
|
|
mTrimLogs = config.value( "trim_logs", false );
|
|
else if ( updateConfigFile )
|
|
config["trim_logs"] = mTrimLogs;
|
|
|
|
if ( config.contains( "hover_delay" ) )
|
|
setHoverDelay( Time::fromString( config["hover_delay"].get<std::string>() ) );
|
|
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<std::string>() ) );
|
|
else if ( updateConfigFile )
|
|
config["server_close_after_idle_time"] = mClientManager.getLSPDecayTime().toString();
|
|
|
|
if ( config.contains( "semantic_highlighting" ) )
|
|
mSemanticHighlighting = config.value( "semantic_highlighting", true );
|
|
else if ( updateConfigFile )
|
|
config["semantic_highlighting"] = mSemanticHighlighting;
|
|
|
|
if ( config.contains( "disable_semantic_highlighting_lang" ) ) {
|
|
try {
|
|
mSemanticHighlightingDisabledLangs.clear();
|
|
const auto& langs = config["disable_semantic_highlighting_lang"];
|
|
for ( const auto& lang : langs ) {
|
|
if ( lang.is_string() ) {
|
|
std::string lg = lang.get<std::string>();
|
|
if ( !lg.empty() )
|
|
mSemanticHighlightingDisabledLangs.insert( lg );
|
|
}
|
|
}
|
|
} catch ( const json::exception& e ) {
|
|
Log::debug( "LSPClientPlugin::loadLSPConfig: Error parsing "
|
|
"disable_semantic_highlighting_lang: %s",
|
|
e.what() );
|
|
}
|
|
} else {
|
|
config["disable_semantic_highlighting_lang"] = json::array();
|
|
}
|
|
|
|
if ( config.contains( "breadcrumb_navigation" ) )
|
|
mBreadcrumb = config.value( "breadcrumb_navigation", true );
|
|
else if ( updateConfigFile )
|
|
config["breadcrumb_navigation"] = mBreadcrumb;
|
|
|
|
if ( config.contains( "breadcrumb_height" ) )
|
|
mBreadcrumbHeight = config.value( "breadcrumb_height", "20dp" );
|
|
else if ( updateConfigFile )
|
|
config["breadcrumb_height"] = mBreadcrumbHeight.toString();
|
|
}
|
|
|
|
if ( mKeyBindings.empty() ) {
|
|
mKeyBindings["lsp-go-to-definition"] = "f2";
|
|
mKeyBindings["lsp-go-to-implementation"] = "shift+f2";
|
|
mKeyBindings["lsp-symbol-info"] = "f1";
|
|
mKeyBindings["lsp-symbol-code-action"] = "alt+return";
|
|
mKeyBindings["lsp-rename-symbol-under-cursor"] = "mod+shift+r";
|
|
mKeyBindings["lsp-symbol-references"] = "mod+shift+u";
|
|
mKeyBindings["lsp-format-range"] = "alt+shift+f";
|
|
mKeyBindings["lsp-show-document-symbols"] = "mod+shift+l";
|
|
}
|
|
|
|
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",
|
|
"lsp-symbol-references",
|
|
"lsp-memory-usage",
|
|
"lsp-symbol-code-action",
|
|
"lsp-rename-symbol-under-cursor",
|
|
"lsp-refresh-semantic-highlighting",
|
|
"lsp-format-range",
|
|
"lsp-plugin-restart",
|
|
"lsp-show-document-symbols" };
|
|
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 ) {
|
|
std::string newData( j.dump( 2 ) );
|
|
if ( newData != data ) {
|
|
FileSystem::fileWrite( path, newData );
|
|
mConfigHash = String::hash( newData );
|
|
}
|
|
}
|
|
|
|
if ( !j.contains( "servers" ) )
|
|
return;
|
|
auto& servers = j["servers"];
|
|
|
|
for ( auto& obj : servers ) {
|
|
bool lspOverwritten = false;
|
|
// Allow disabling an LSP by redeclaring it in the user configuration file.
|
|
if ( updateConfigFile && ( obj.contains( "name" ) || obj.contains( "use" ) ) &&
|
|
obj.contains( "disabled" ) && obj.at( "disabled" ).is_boolean() ) {
|
|
std::string name = obj.contains( "name" ) ? obj["name"] : obj["use"];
|
|
for ( auto& lspD : lsps ) {
|
|
if ( lspD.name == name ) {
|
|
lspD.disabled = obj["disabled"].get<bool>();
|
|
lspOverwritten = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Allow overriding the command for already defined LSP
|
|
// And allow adding parameters to the already defined LSP
|
|
if ( updateConfigFile && ( obj.contains( "name" ) || obj.contains( "use" ) ) &&
|
|
( obj.contains( "command" ) ||
|
|
( obj.contains( "command_parameters" ) &&
|
|
obj.at( "command_parameters" ).is_string() ) ||
|
|
( obj.contains( "host" ) && obj.at( "host" ).is_string() &&
|
|
obj.contains( "port " ) && obj.at( "port" ).is_number_integer() ) ||
|
|
obj.contains( "settings" ) || obj.contains( "initializationOptions" ) ) ) {
|
|
for ( auto& lspR : lsps ) {
|
|
std::string name = obj.contains( "name" ) ? obj["name"] : obj["use"];
|
|
if ( lspR.name == name ) {
|
|
lspOverwritten = true;
|
|
if ( obj.contains( "use" ) && obj["use"].is_string() )
|
|
lspR.usesLSP = obj["use"].get<std::string>();
|
|
if ( obj.contains( "share_process" ) && obj["share_process"].is_boolean() ) {
|
|
lspR.shareProcessWithOtherDefinition = obj["share_process"].get<bool>();
|
|
}
|
|
if ( obj.contains( "command" ) ) {
|
|
lspR.command =
|
|
parseCommand( obj["command"], mManager->getWorkspaceFolder() );
|
|
}
|
|
if ( !obj.value( "command_parameters", "" ).empty() ) {
|
|
std::string cmdParam( obj.value( "command_parameters", "" ) );
|
|
if ( !cmdParam.empty() && cmdParam.front() != ' ' )
|
|
cmdParam = " " + cmdParam;
|
|
lspR.commandParameters += cmdParam;
|
|
LSPClientServer::sanitizeCommand( lspR.commandParameters,
|
|
mManager->getWorkspaceFolder() );
|
|
}
|
|
if ( obj.contains( "host" ) ) {
|
|
lspR.host = obj.value( "host", "" );
|
|
lspR.port = obj.value( "port", 0 );
|
|
}
|
|
if ( obj.contains( "settings" ) ) {
|
|
lspR.settings = obj["settings"];
|
|
}
|
|
if ( obj.contains( "initializationOptions" ) ) {
|
|
lspR.initializationOptions = obj["initializationOptions"];
|
|
}
|
|
tryAddEnv( obj, lspR );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( lspOverwritten )
|
|
continue;
|
|
|
|
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 = parseCommand( tlsp.command, mManager->getWorkspaceFolder() );
|
|
lsp.name = tlsp.name;
|
|
lsp.rootIndicationFileNames = tlsp.rootIndicationFileNames;
|
|
lsp.url = tlsp.url;
|
|
lsp.host = tlsp.host;
|
|
lsp.port = tlsp.port;
|
|
lsp.initializationOptions = tlsp.initializationOptions;
|
|
lsp.settings = tlsp.settings;
|
|
lsp.extraTriggerChars = tlsp.extraTriggerChars;
|
|
lsp.usesLSP = use;
|
|
if ( obj.contains( "share_process" ) && obj["share_process"].is_boolean() ) {
|
|
lsp.shareProcessWithOtherDefinition = obj["share_process"].get<bool>();
|
|
}
|
|
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 = parseCommand( obj["command"], mManager->getWorkspaceFolder() );
|
|
lsp.name = obj["name"];
|
|
lsp.url = obj.value( "url", "" );
|
|
lsp.host = obj.value( "host", "" );
|
|
lsp.port = obj.value( "port", 0 );
|
|
lsp.shareProcessWithOtherDefinition = obj.value( "share_process", false );
|
|
if ( obj.contains( "vars" ) && obj.is_object() ) {
|
|
auto vars = obj["vars"];
|
|
for ( const auto& var : vars.items() ) {
|
|
if ( var.value().is_string() )
|
|
lsp.cmdVars[var.key()] = var.value().get<std::string>();
|
|
}
|
|
}
|
|
}
|
|
|
|
lsp.commandParameters = obj.value( "command_parameters", lsp.commandParameters );
|
|
|
|
if ( obj.contains( "initializationOptions" ) )
|
|
lsp.initializationOptions = obj["initializationOptions"];
|
|
|
|
if ( obj.contains( "settings" ) )
|
|
lsp.settings = obj["settings"];
|
|
|
|
auto& fp = obj["file_patterns"];
|
|
|
|
for ( auto& pattern : fp )
|
|
if ( pattern.is_string() )
|
|
lsp.filePatterns.push_back( pattern.get<std::string>() );
|
|
|
|
if ( obj.contains( "rootIndicationFileNames" ) ) {
|
|
lsp.rootIndicationFileNames.clear();
|
|
auto& fnms = obj["rootIndicationFileNames"];
|
|
for ( auto& fn : fnms )
|
|
if ( fn.is_string() )
|
|
lsp.rootIndicationFileNames.push_back( fn );
|
|
}
|
|
|
|
if ( obj.contains( "extra_trigger_chars" ) && obj["extra_trigger_chars"].is_array() ) {
|
|
for ( auto& jch : obj["extra_trigger_chars"] ) {
|
|
if ( jch.is_string() )
|
|
lsp.extraTriggerChars.push_back( jch );
|
|
}
|
|
}
|
|
|
|
LSPClientServer::sanitizeCommand( lsp.command, mManager->getWorkspaceFolder() );
|
|
LSPClientServer::sanitizeCommand( lsp.commandParameters, mManager->getWorkspaceFolder() );
|
|
|
|
tryAddEnv( obj, lsp );
|
|
|
|
// 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<LSPDefinition>& lsps,
|
|
const std::vector<std::string>& 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::getAndGoToLocation( UICodeEditor* editor, const std::string& search ) {
|
|
mClientManager.getAndGoToLocation(
|
|
editor->getDocumentRef(), search,
|
|
[this, editor]( const LSPClientServer::IdType&, const std::vector<LSPLocation>& res ) {
|
|
if ( res.empty() )
|
|
return;
|
|
if ( res.size() == 1 ) {
|
|
mClientManager.goToLocation( res[0] );
|
|
} else {
|
|
createLocationsView( editor, res );
|
|
}
|
|
} );
|
|
}
|
|
|
|
void LSPClientPlugin::processDiagnosticsCodeAction( const PluginMessage& msg ) {
|
|
if ( !( msg.isResponse() && msg.type == PluginMessageType::DiagnosticsCodeAction &&
|
|
msg.format == PluginMessageFormat::DiagnosticsCodeAction ) )
|
|
return;
|
|
|
|
mQuickFix = msg.asDiasnosticsCodeAction();
|
|
}
|
|
|
|
void LSPClientPlugin::codeAction( UICodeEditor* editor ) {
|
|
json j;
|
|
j["uri"] = editor->getDocument().getURI().toString();
|
|
j["pos"] = editor->getDocument().getSelection().start().toString();
|
|
mQuickFix = {};
|
|
auto req = mManager->sendRequest( PluginMessageType::DiagnosticsCodeAction,
|
|
PluginMessageFormat::JSON, &j );
|
|
|
|
json j2;
|
|
j2["uri"] = editor->getDocument().getURI().toString();
|
|
j2["line"] = editor->getDocument().getSelection().start().line();
|
|
j2["character"] = editor->getDocument().getSelection().start().column();
|
|
|
|
auto resp =
|
|
mManager->sendRequest( PluginMessageType::GetDiagnostics, PluginMessageFormat::JSON, &j2 );
|
|
nlohmann::json diagnostics;
|
|
if ( resp.isResponse() )
|
|
diagnostics = std::move( resp.getResponse().data );
|
|
|
|
bool sent = mClientManager.codeAction(
|
|
editor->getDocumentRef(), diagnostics,
|
|
[this, editor]( const LSPClientServer::IdType&, const std::vector<LSPCodeAction>& res ) {
|
|
createCodeActionsView( editor, res );
|
|
} );
|
|
|
|
if ( !sent )
|
|
editor->getDocument().execute( "spellchecker-fix-typo", editor );
|
|
}
|
|
|
|
void LSPClientPlugin::renameSymbol( UICodeEditor* editor ) {
|
|
UIMessageBox* msgBox = UIMessageBox::New(
|
|
UIMessageBox::INPUT, editor->getUISceneNode()->i18n(
|
|
"new_symbol_under_cursor_name",
|
|
"New name (caution: not all references may be replaced)" ) );
|
|
msgBox->setTitle( editor->getUISceneNode()->i18n( "rename", "Rename" ) );
|
|
msgBox->setCloseShortcut( { KEY_ESCAPE, KEYMOD_NONE } );
|
|
auto selection = editor->getDocument().getSelection();
|
|
TextPosition selStart = selection.start();
|
|
msgBox->getTextInput()->setText( editor->getDocument().getWordInPosition(
|
|
selection.hasSelection() ? selection.end() : selStart ) );
|
|
msgBox->getTextInput()->getDocument().selectAll();
|
|
msgBox->showWhenReady();
|
|
msgBox->on( Event::OnConfirm, [this, selStart, editor, msgBox]( const Event* ) {
|
|
String newName( msgBox->getTextInput()->getText() );
|
|
mClientManager.renameSymbol( editor->getDocumentRef()->getURI(), selStart, newName );
|
|
msgBox->closeWindow();
|
|
} );
|
|
msgBox->on( Event::OnClose, [this]( const Event* ) {
|
|
if ( mManager->getSplitter() && mManager->getSplitter()->getCurWidget() )
|
|
mManager->getSplitter()->getCurWidget()->setFocus();
|
|
} );
|
|
}
|
|
|
|
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() ) {
|
|
auto& doc = editor->getDocument();
|
|
|
|
doc.setCommand( "lsp-go-to-definition", [this]( TextDocument::Client* client ) {
|
|
getAndGoToLocation( static_cast<UICodeEditor*>( client ), "textDocument/definition" );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-rename-symbol-under-cursor", [this]( TextDocument::Client* client ) {
|
|
renameSymbol( static_cast<UICodeEditor*>( client ) );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-go-to-declaration", [this]( TextDocument::Client* client ) {
|
|
getAndGoToLocation( static_cast<UICodeEditor*>( client ), "textDocument/declaration" );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-go-to-implementation", [this]( TextDocument::Client* client ) {
|
|
getAndGoToLocation( static_cast<UICodeEditor*>( client ),
|
|
"textDocument/implementation" );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-go-to-type-definition", [this]( TextDocument::Client* client ) {
|
|
getAndGoToLocation( static_cast<UICodeEditor*>( client ),
|
|
"textDocument/typeDefinition" );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-switch-header-source", [this]( TextDocument::Client* client ) {
|
|
switchSourceHeader( static_cast<UICodeEditor*>( client ) );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-symbol-info", [this]( TextDocument::Client* client ) {
|
|
getSymbolInfo( static_cast<UICodeEditor*>( client ) );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-symbol-references", [this]( TextDocument::Client* client ) {
|
|
mClientManager.getSymbolReferences(
|
|
static_cast<UICodeEditor*>( client )->getDocumentRef() );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-symbol-code-action", [this]( TextDocument::Client* client ) {
|
|
codeAction( static_cast<UICodeEditor*>( client ) );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-memory-usage", [this]( TextDocument::Client* client ) {
|
|
mClientManager.memoryUsage( static_cast<UICodeEditor*>( client )->getDocumentRef() );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-refresh-semantic-highlighting",
|
|
[this]( TextDocument::Client* client ) {
|
|
mClientManager.requestSemanticHighlighting(
|
|
static_cast<UICodeEditor*>( client )->getDocumentRef() );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-format-range", [this]( TextDocument::Client* client ) {
|
|
mClientManager.rangeFormatting(
|
|
static_cast<UICodeEditor*>( client )->getDocumentRef() );
|
|
} );
|
|
|
|
doc.setCommand( "lsp-plugin-restart", [this] { mManager->reload( getId() ); } );
|
|
|
|
doc.setCommand( "lsp-show-document-symbols", [this]( TextDocument::Client* client ) {
|
|
showDocumentSymbols( static_cast<UICodeEditor*>( client ) );
|
|
} );
|
|
}
|
|
|
|
std::vector<Uint32> listeners;
|
|
|
|
listeners.push_back( editor->on( Event::OnDocumentLoaded, [this, editor]( const Event* ) {
|
|
mEditorDocs[editor] = editor->getDocumentRef().get();
|
|
mClientManager.run( editor->getDocumentRef() );
|
|
updateCurrentSymbol( editor->getDocument() );
|
|
} ) );
|
|
|
|
listeners.push_back( editor->on( Event::OnCursorPosChange, [this, editor]( const Event* ) {
|
|
if ( mSymbolInfoShowing )
|
|
hideTooltip( editor );
|
|
updateCurrentSymbol( editor->getDocument() );
|
|
} ) );
|
|
|
|
listeners.push_back(
|
|
editor->on( Event::OnDocumentChanged, [this, editor]( const Event* event ) {
|
|
const DocChangedEvent* docChangedEvent = static_cast<const DocChangedEvent*>( event );
|
|
TextDocument* oldDoc = mEditorDocs[editor];
|
|
TextDocument* newDoc = editor->getDocumentRef().get();
|
|
{
|
|
Lock l( mDocMutex );
|
|
mDocs.erase( oldDoc );
|
|
mEditorDocs[editor] = newDoc;
|
|
}
|
|
|
|
{
|
|
Lock l( mDocCurrentSymbolsMutex );
|
|
mDocCurrentSymbols.erase( docChangedEvent->getOldDocURI() );
|
|
}
|
|
|
|
updateCurrentSymbol( editor->getDocument() );
|
|
} ) );
|
|
|
|
if ( mBreadcrumb ) {
|
|
mPluginTopSpace = mBreadcrumbHeight.asPixels(
|
|
getUISceneNode()->getPixelsSize().getWidth(), getUISceneNode()->getPixelsSize(),
|
|
getUISceneNode()->getDPI(), getUISceneNode()->getUIThemeManager()->getDefaultFontSize(),
|
|
getUISceneNode()->getUIThemeManager()->getDefaultFontSize() );
|
|
editor->registerTopSpace( this, mPluginTopSpace, 0 );
|
|
}
|
|
|
|
mEditors.insert( { editor, listeners } );
|
|
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(),
|
|
[this, editor]( const Int64&, const LSPHover& resp ) {
|
|
onDocumentHoverResponse( editor, resp, false );
|
|
} );
|
|
}
|
|
|
|
void LSPClientPlugin::onUnregister( UICodeEditor* editor ) {
|
|
for ( auto& kb : mKeyBindings )
|
|
editor->getKeyBindings().removeCommandKeybind( kb.first );
|
|
|
|
if ( mShuttingDown )
|
|
return;
|
|
|
|
editor->removeActionsByTag( getMouseMoveHash( editor ) );
|
|
|
|
URI docURI;
|
|
{
|
|
Lock l( mDocMutex );
|
|
TextDocument* doc = &editor->getDocument();
|
|
docURI = doc->getURI();
|
|
const auto& cbs = mEditors[editor];
|
|
for ( auto listener : cbs )
|
|
editor->removeEventListener( listener );
|
|
|
|
if ( mBreadcrumb )
|
|
editor->unregisterTopSpace( this );
|
|
|
|
mEditors.erase( editor );
|
|
mEditorDocs.erase( editor );
|
|
for ( const auto& ieditor : mEditorDocs ) {
|
|
if ( ieditor.second == doc )
|
|
return;
|
|
}
|
|
|
|
if ( editor->hasDocument() )
|
|
for ( auto& kb : mKeyBindings )
|
|
editor->getDocument().removeCommand( kb.first );
|
|
|
|
{
|
|
Lock lds( mDocSymbolsMutex );
|
|
mDocSymbols.erase( doc->getURI() );
|
|
mDocFlatSymbols.erase( doc->getURI() );
|
|
}
|
|
|
|
mDocs.erase( doc );
|
|
}
|
|
|
|
{
|
|
Lock l2( mDocCurrentSymbolsMutex );
|
|
mDocCurrentSymbols.erase( docURI );
|
|
}
|
|
}
|
|
|
|
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, menu]( const std::string& cmd, const std::string& text,
|
|
const std::string& icon = "" ) {
|
|
menu->add( text, iconDrawable( icon, 12 ), KeyBindings::keybindFormat( mKeyBindings[cmd] ) )
|
|
->setId( cmd );
|
|
};
|
|
auto& cap = server->getCapabilities();
|
|
|
|
addFn( "lsp-symbol-info", i18n( "lsp_symbol_info", "Symbol Info" ) );
|
|
|
|
if ( cap.definitionProvider )
|
|
addFn( "lsp-go-to-definition", i18n( "lsp_go_to_definition", "Go To Definition" ) );
|
|
|
|
if ( cap.declarationProvider )
|
|
addFn( "lsp-go-to-declaration", i18n( "lsp_go_to_declaration", "Go To Declaration" ) );
|
|
|
|
if ( cap.typeDefinitionProvider )
|
|
addFn( "lsp-go-to-type-definition",
|
|
i18n( "lsp_go_to_type_definition", "Go To Type Definition" ) );
|
|
|
|
if ( cap.implementationProvider )
|
|
addFn( "lsp-go-to-implementation",
|
|
i18n( "lsp_go_to_implementation", "Go To Implementation" ) );
|
|
|
|
if ( cap.renameProvider )
|
|
addFn( "lsp-rename-symbol-under-cursor",
|
|
i18n( "lsp_rename_symbol_under_cursor", "Rename Symbol Under Cursor" ) );
|
|
|
|
if ( cap.referencesProvider )
|
|
addFn( "lsp-symbol-references", i18n( "lsp_find_references_to_symbol_under_cursor",
|
|
"Find References to Symbol Under Cursor" ) );
|
|
|
|
if ( cap.codeActionProvider )
|
|
addFn( "lsp-symbol-code-action", i18n( "lsp_code_action", "Code Action" ),
|
|
"lightbulb-autofix" );
|
|
|
|
if ( cap.documentRangeFormattingProvider &&
|
|
editor->getDocument().getSelection().hasSelection() )
|
|
addFn( "lsp-format-range", i18n( "lsp_format_selected_range", "Format Selected Range" ) );
|
|
|
|
if ( cap.semanticTokenProvider.full || cap.semanticTokenProvider.fullDelta )
|
|
addFn( "lsp-refresh-semantic-highlighting",
|
|
i18n( "lsp_refresh_semantic_highlighting", "Refresh Semantic Highlighting" ),
|
|
"refresh" );
|
|
|
|
if ( cap.documentSymbolProvider )
|
|
addFn( "lsp-show-document-symbols",
|
|
i18n( "show-document-symbols", "Show Document Symbols" ), "symbol-function" );
|
|
|
|
if ( server->getDefinition().language == "cpp" || server->getDefinition().language == "c" )
|
|
addFn( "lsp-switch-header-source",
|
|
i18n( "lsp_switch_header_source", "Switch Header/Source" ), "filetype-hpp" );
|
|
|
|
addFn( "lsp-plugin-restart", i18n( "lsp_restart_lsp_server", "Restart LSP Server" ),
|
|
"refresh" );
|
|
|
|
#ifdef EE_DEBUG
|
|
if ( server->getDefinition().name == "clangd" )
|
|
addFn( "lsp-memory-usage", i18n( "lsp_memory_usage", "LSP Memory Usage" ) );
|
|
#endif
|
|
|
|
return false;
|
|
}
|
|
|
|
void LSPClientPlugin::hideTooltip( UICodeEditor* editor ) {
|
|
mSymbolInfoShowing = false;
|
|
UITooltip* tooltip = nullptr;
|
|
if ( editor && ( tooltip = editor->getTooltip() ) && tooltip->isVisible() &&
|
|
tooltip->getData() == String::hash( "lsp" ) ) {
|
|
editor->setTooltipText( "" );
|
|
tooltip->hide();
|
|
// Restore old tooltip state
|
|
tooltip->setData( 0 );
|
|
tooltip->setFontStyle( mOldTextStyle );
|
|
tooltip->setHorizontalAlign( mOldTextAlign );
|
|
tooltip->setUsingCustomStyling( mOldUsingCustomStyling );
|
|
tooltip->setDontAutoHideOnMouseMove( mOldDontAutoHideOnMouseMove );
|
|
tooltip->setBackgroundColor( mOldBackgroundColor );
|
|
tooltip->setWordWrap( mOldWordWrap );
|
|
tooltip->setMaxWidthEq( mOldMaxWidth );
|
|
}
|
|
}
|
|
|
|
static TextPosition currentMouseTextPosition( UICodeEditor* editor ) {
|
|
return editor->resolveScreenPosition( editor->getInput()->getMousePos().asFloat() );
|
|
}
|
|
|
|
void LSPClientPlugin::tryHideTooltip( UICodeEditor* editor, const Vector2i& position ) {
|
|
if ( ( editor->hasDocument() && editor->getDocument().isLoading() ) ||
|
|
!mCurrentHover.range.isValid() ||
|
|
( mCurrentHover.range.isValid() &&
|
|
!mCurrentHover.range.contains( editor->resolveScreenPosition( position.asFloat() ) ) ) )
|
|
hideTooltip( editor );
|
|
}
|
|
|
|
void LSPClientPlugin::displayTooltip( UICodeEditor* editor, const LSPHover& resp,
|
|
const Vector2f& position ) {
|
|
// HACK: Gets the old font style to restore it when the tooltip is hidden
|
|
UITooltip* tooltip = editor->createTooltip();
|
|
if ( tooltip == nullptr )
|
|
return;
|
|
mOldWordWrap = tooltip->isWordWrap();
|
|
mOldMaxWidth = tooltip->getMaxWidthEq();
|
|
tooltip->setWordWrap( true );
|
|
tooltip->setMaxWidthEq( "50vw" );
|
|
editor->setTooltipText( resp.contents[0].value );
|
|
mOldTextStyle = tooltip->getFontStyle();
|
|
mOldTextAlign = tooltip->getHorizontalAlign();
|
|
mOldDontAutoHideOnMouseMove = tooltip->dontAutoHideOnMouseMove();
|
|
mOldUsingCustomStyling = tooltip->getUsingCustomStyling();
|
|
mOldBackgroundColor = tooltip->getBackgroundColor();
|
|
if ( Color::Transparent == mOldBackgroundColor ) {
|
|
tooltip->reloadStyle( true, true, true, true );
|
|
mOldBackgroundColor = tooltip->getBackgroundColor();
|
|
}
|
|
tooltip->setHorizontalAlign( UI_HALIGN_LEFT );
|
|
tooltip->setPixelsPosition( tooltip->getTooltipPosition( position ) );
|
|
tooltip->setDontAutoHideOnMouseMove( true );
|
|
tooltip->setUsingCustomStyling( true );
|
|
tooltip->setFontStyle( Text::Regular );
|
|
tooltip->setData( String::hash( "lsp" ) );
|
|
tooltip->setBackgroundColor( editor->getColorScheme().getEditorColor( "background"_sst ) );
|
|
tooltip->getUIStyle()->setStyleSheetProperty( StyleSheetProperty(
|
|
"background-color",
|
|
editor->getColorScheme().getEditorColor( "background"_sst ).toHexString(), true,
|
|
StyleSheetSelectorRule::SpecificityImportant ) );
|
|
|
|
if ( tooltip->getText().empty() )
|
|
return;
|
|
|
|
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->getTextCache()->getString().empty() )
|
|
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() );
|
|
}
|
|
|
|
void LSPClientPlugin::onDocumentHoverResponse( UICodeEditor* editor, const LSPHover& resp,
|
|
bool fromMousePos,
|
|
std::optional<Vector2f> screenPos ) {
|
|
const auto& displayTooltipFn = [this, fromMousePos]( UICodeEditor* editor, const LSPHover& resp,
|
|
const Vector2f& position ) {
|
|
if ( fromMousePos )
|
|
tryDisplayTooltip( editor, resp, position.asInt() );
|
|
else
|
|
displayTooltip( editor, resp, position );
|
|
};
|
|
|
|
const auto& getFinalPosFn = [screenPos]( UICodeEditor* editor ) {
|
|
return screenPos ? *screenPos
|
|
: editor->getScreenPosition( editor->getDocument().getSelection().start() )
|
|
.getPosition();
|
|
};
|
|
|
|
auto& doc = editor->getDocument();
|
|
json j;
|
|
TextPosition textPosition =
|
|
fromMousePos ? currentMouseTextPosition( editor ) : doc.getSelection().start();
|
|
j["uri"] = doc.getURI().toString();
|
|
j["line"] = textPosition.line();
|
|
j["character"] = textPosition.column();
|
|
auto errResp = mManager->sendRequest( PluginMessageType::GetErrorOrWarning,
|
|
PluginMessageFormat::JSON, &j );
|
|
if ( !resp.contents.empty() && !resp.contents[0].value.empty() ) {
|
|
editor->runOnMainThread( [editor, resp, errResp, this, screenPos, displayTooltipFn,
|
|
getFinalPosFn]() {
|
|
mSymbolInfoShowing = true;
|
|
auto pos =
|
|
screenPos
|
|
? *screenPos
|
|
: editor->getScreenPosition( editor->getDocument().getSelection().start() )
|
|
.getPosition();
|
|
if ( errResp.isResponse() ) {
|
|
LSPHover cresp( resp );
|
|
cresp.contents[0].value = errResp.getResponse().data["text"].get<std::string>() +
|
|
"\n---\n\n" + cresp.contents[0].value;
|
|
displayTooltipFn( editor, cresp, getFinalPosFn( editor ) );
|
|
} else {
|
|
displayTooltipFn( editor, resp, pos );
|
|
}
|
|
} );
|
|
} else if ( errResp.isResponse() ) {
|
|
LSPHover tresp;
|
|
tresp.range =
|
|
TextRange::fromString( errResp.getResponse().data["range"].get<std::string>() );
|
|
tresp.contents.push_back(
|
|
{ LSPMarkupKind::MarkDown, errResp.getResponse().data["text"].get<std::string>() } );
|
|
editor->runOnMainThread( [editor, tresp, this, displayTooltipFn, getFinalPosFn]() {
|
|
mSymbolInfoShowing = true;
|
|
displayTooltipFn( editor, tresp, getFinalPosFn( editor ) );
|
|
} );
|
|
} else {
|
|
json data = getURIAndPositionJSON( editor );
|
|
mManager->sendRequest( PluginMessageType::SignatureHelp, PluginMessageFormat::JSON, &data );
|
|
}
|
|
}
|
|
|
|
bool LSPClientPlugin::onMouseMove( UICodeEditor* editor, const Vector2i& position,
|
|
const Uint32& flags ) {
|
|
auto localPos( editor->convertToNodeSpace( position.asFloat() ) );
|
|
if ( localPos.x <= editor->getGutterWidth() )
|
|
return false;
|
|
|
|
if ( mBreadcrumb ) {
|
|
if ( localPos.y < mPluginTopSpace && localPos.x < editor->getTopAreaWidth() &&
|
|
localPos.x >= 0 ) {
|
|
if ( !mHoveringBreadcrumb ) {
|
|
mHoveringBreadcrumb = editor;
|
|
editor->invalidateDraw();
|
|
}
|
|
getUISceneNode()->setCursor( Cursor::Hand );
|
|
return false;
|
|
}
|
|
}
|
|
if ( mHoveringBreadcrumb ) {
|
|
mHoveringBreadcrumb = nullptr;
|
|
editor->invalidateDraw();
|
|
}
|
|
|
|
if ( flags != 0 ) {
|
|
tryHideTooltip( editor, position );
|
|
return false;
|
|
}
|
|
|
|
editor->debounce(
|
|
[this, editor]() {
|
|
if ( !editorExists( editor ) )
|
|
return;
|
|
auto server = mClientManager.getOneLSPClientServer( editor );
|
|
if ( server == nullptr || !server->getCapabilities().hoverProvider )
|
|
return;
|
|
server->documentHover(
|
|
editor->getDocument().getURI(), currentMouseTextPosition( editor ),
|
|
[this, editor]( const Int64&, const LSPHover& resp ) {
|
|
if ( editorExists( editor ) && !resp.contents.empty() &&
|
|
!resp.contents[0].value.empty() ) {
|
|
editor->runOnMainThread( [editor, resp, this]() {
|
|
auto mousePos = editor->getUISceneNode()
|
|
->getWindow()
|
|
->getInput()
|
|
->getMousePos()
|
|
.asFloat();
|
|
if ( !editor->getScreenRect().contains( mousePos ) )
|
|
return;
|
|
onDocumentHoverResponse( editor, resp, true, mousePos );
|
|
} );
|
|
}
|
|
} );
|
|
},
|
|
mHoverDelay, getMouseMoveHash( editor ) );
|
|
tryHideTooltip( editor, position );
|
|
return editor->getTooltip() && editor->getTooltip()->isVisible() && mSymbolInfoShowing;
|
|
}
|
|
|
|
bool LSPClientPlugin::onMouseLeave( UICodeEditor* editor, const Vector2i&, const Uint32& ) {
|
|
if ( mHoveringBreadcrumb ) {
|
|
mHoveringBreadcrumb = nullptr;
|
|
editor->invalidateDraw();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void LSPClientPlugin::onVersionUpgrade( Uint32 oldVersion, Uint32 ) {
|
|
if ( oldVersion <= ECODE_VERSIONNUM( 0, 5, 0, 0 ) ) {
|
|
mSemanticHighlighting = true;
|
|
}
|
|
}
|
|
|
|
void LSPClientPlugin::drawTop( UICodeEditor* editor, const Vector2f& screenStart, const Sizef& size,
|
|
const Float& /*fontSize*/ ) {
|
|
Float width = editor->getTopAreaWidth();
|
|
Primitives p;
|
|
Color backColor( editor->getColorScheme().getEditorColor( SyntaxStyleTypes::Background ) );
|
|
p.setColor( backColor );
|
|
p.drawRectangle( Rectf( screenStart, Sizef( width, mPluginTopSpace ) ) );
|
|
|
|
Color lineColor( editor->getColorScheme().getEditorColor( SyntaxStyleTypes::LineBreakColumn ) );
|
|
p.setColor( lineColor );
|
|
Float lineHeight = eefloor( PixelDensity::dpToPxI( 1 ) );
|
|
p.drawRectangle( { { screenStart.x, screenStart.y + size.getHeight() - lineHeight },
|
|
Sizef( width, lineHeight ) } );
|
|
|
|
Font* font = getUISceneNode()->getUIThemeManager()->getDefaultFont();
|
|
if ( !font )
|
|
return;
|
|
|
|
Float fontSize = editor->getUISceneNode()->getUIThemeManager()->getDefaultFontSize();
|
|
|
|
// Avoid heap allocating
|
|
static constexpr auto MAX_PATH_SIZE = 512;
|
|
String::StringBaseType utf32Path[MAX_PATH_SIZE];
|
|
std::memset( utf32Path, 0, MAX_PATH_SIZE * sizeof( String::StringBaseType ) );
|
|
|
|
const auto& workspace = getManager()->getWorkspaceFolder();
|
|
std::string_view path( editor->getDocument().getFilePath() );
|
|
if ( !workspace.empty() && String::startsWith( path, workspace ) )
|
|
path = path.substr( workspace.size() );
|
|
if ( path.size() > MAX_PATH_SIZE )
|
|
path = path.substr( path.size() - MAX_PATH_SIZE );
|
|
|
|
size_t pathLen = String::toUtf32( path, &utf32Path[0], MAX_PATH_SIZE );
|
|
|
|
Color textColor( editor->getColorScheme().getEditorColor(
|
|
mHoveringBreadcrumb == editor ? SyntaxStyleTypes::Text : SyntaxStyleTypes::LineNumber2 ) );
|
|
Float textOffsetY = eefloor( ( size.getHeight() - font->getLineSpacing( fontSize ) ) * 0.5f );
|
|
|
|
Vector2f pos( screenStart.x + eefloor( PixelDensity::dpToPx( 8 ) ),
|
|
screenStart.y + textOffsetY );
|
|
|
|
String::View pathView{ utf32Path, pathLen };
|
|
auto drawn = Text::draw( String::View( utf32Path, pathLen ), pos, font, fontSize, textColor, 0,
|
|
0.f, Color::Black, Color::Black, { 1, 1 }, 4,
|
|
String::isAscii( pathView ) ? TextHints::AllAscii : 0 );
|
|
|
|
Lock l( mDocCurrentSymbolsMutex );
|
|
auto symbolsInfoIt = mDocCurrentSymbols.find( editor->getDocument().getURI() );
|
|
if ( symbolsInfoIt == mDocCurrentSymbols.end() )
|
|
return;
|
|
|
|
pos.x += drawn.getWidth();
|
|
if ( mDrawSepIcon == nullptr )
|
|
mDrawSepIcon = getUISceneNode()->findIcon( "chevron-right" );
|
|
Float textHeight = drawn.getHeight();
|
|
|
|
const auto& symbolsInfo = symbolsInfoIt->second;
|
|
|
|
for ( const auto& info : symbolsInfo ) {
|
|
if ( mDrawSepIcon ) {
|
|
pos.x += eefloor( PixelDensity::dpToPx( 8 ) );
|
|
Float iconSize = PixelDensity::dpToPxI( drawn.getHeight() * 0.5f );
|
|
auto iconDrawable = mDrawSepIcon->getSize( iconSize );
|
|
Color c = iconDrawable->getColor();
|
|
iconDrawable->setColor( textColor );
|
|
Float iconHeight = iconDrawable->getPixelsSize().getHeight();
|
|
Vector2f iconPos( { pos.x, screenStart.y + textOffsetY +
|
|
eefloor( ( textHeight - iconHeight ) * 0.5f ) } );
|
|
iconDrawable->draw( iconPos );
|
|
pos.x +=
|
|
iconDrawable->getPixelsSize().getWidth() + eefloor( PixelDensity::dpToPx( 8 ) );
|
|
iconDrawable->setColor( c );
|
|
} else {
|
|
pos.x += eefloor( PixelDensity::dpToPx( 16 ) );
|
|
}
|
|
|
|
UIIcon* iconKind = getUISceneNode()->findIcon( info.icon );
|
|
if ( iconKind ) {
|
|
auto iconDrawable = iconKind->getSize( fontSize );
|
|
Color c = iconDrawable->getColor();
|
|
iconDrawable->setColor( textColor );
|
|
Float iconHeight = iconDrawable->getPixelsSize().getHeight();
|
|
iconDrawable->draw( { pos.x, screenStart.y + textOffsetY +
|
|
eefloor( ( textHeight - iconHeight ) * 0.5f ) } );
|
|
pos.x += iconDrawable->getPixelsSize().getWidth() + PixelDensity::dpToPxI( 4 );
|
|
iconDrawable->setColor( c );
|
|
}
|
|
|
|
drawn =
|
|
Text::draw( info.name, pos, font, fontSize, textColor, 0, 0.f, Color::Black,
|
|
Color::Black, { 1, 1 }, 4, info.name.isAscii() ? TextHints::AllAscii : 0 );
|
|
pos.x += drawn.getWidth();
|
|
}
|
|
}
|
|
|
|
void LSPClientPlugin::updateCurrentSymbol( TextDocument& doc ) {
|
|
if ( !mBreadcrumb || mShuttingDown )
|
|
return;
|
|
|
|
std::vector<DisplaySymbolInfo> symbolsInfo;
|
|
URI uri = doc.getURI();
|
|
|
|
{
|
|
Lock l( mDocSymbolsMutex );
|
|
auto symbolsIt = mDocSymbols.find( uri );
|
|
if ( symbolsIt != mDocSymbols.end() ) {
|
|
LSPSymbolInformationList* list = &symbolsIt->second;
|
|
auto sel = doc.getSelection();
|
|
LSPSymbolInformationList::iterator foundIt;
|
|
|
|
bool found = false;
|
|
do {
|
|
foundIt =
|
|
std::lower_bound( list->begin(), list->end(), sel,
|
|
[]( const LSPSymbolInformation& cur, const TextRange& sel ) {
|
|
return cur.range < sel;
|
|
} );
|
|
found = foundIt != list->end() && foundIt->range.contains( sel );
|
|
if ( found ) {
|
|
symbolsInfo.push_back( { String::fromUtf8( foundIt->name ),
|
|
LSPSymbolKindHelper::toIconString( foundIt->kind ) } );
|
|
if ( foundIt->children.empty() )
|
|
break;
|
|
list = &foundIt->children;
|
|
} else
|
|
break;
|
|
} while ( found );
|
|
}
|
|
}
|
|
|
|
Lock l( mDocCurrentSymbolsMutex );
|
|
auto prevSymbols = mDocCurrentSymbols.find( uri );
|
|
if ( prevSymbols == mDocCurrentSymbols.end() || prevSymbols->second != symbolsInfo ) {
|
|
mDocCurrentSymbols[uri] = std::move( symbolsInfo );
|
|
getManager()->getSplitter()->forEachEditor( [&uri]( UICodeEditor* editor ) {
|
|
if ( editor->isVisible() && editor->getDocument().getURI() == uri )
|
|
editor->invalidateDraw();
|
|
} );
|
|
}
|
|
}
|
|
|
|
void LSPClientPlugin::showDocumentSymbols( UICodeEditor* editor ) {
|
|
TextDocument& doc = editor->getDocument();
|
|
URI uri = doc.getURI();
|
|
|
|
auto model = createDocSymbolsModel( uri );
|
|
if ( !model )
|
|
return;
|
|
|
|
getUISceneNode()->getRoot()->setFocus();
|
|
UIWindow* win =
|
|
UIWindow::NewOpt( UIMessageBox::WindowBaseContainerType::VERTICAL_LINEAR_LAYOUT );
|
|
win->setMinWindowSize( 400, getUISceneNode()->getSize().getHeight() * 0.7f );
|
|
win->setKeyBindingCommand( "closeWindow", [win, editor] {
|
|
win->closeWindow();
|
|
editor->setFocus();
|
|
} );
|
|
win->getKeyBindings().addKeybind( { KEY_ESCAPE }, "closeWindow" );
|
|
win->setWindowFlags( UI_WIN_NO_DECORATION | UI_WIN_SHADOW | UI_WIN_MODAL | UI_WIN_EPHEMERAL );
|
|
win->center();
|
|
UITextInput* input = UITextInput::New();
|
|
input->setParent( win->getContainer() );
|
|
input->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::WrapContent );
|
|
input->setHint( i18n( "search_symbols_ellipsis", "Search Symbols..." ) );
|
|
|
|
UITreeView* tv = UITreeView::New();
|
|
tv->setHeadersVisible( false );
|
|
tv->setAutoExpandOnSingleColumn( true );
|
|
tv->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::Fixed );
|
|
tv->setLayoutWeight( 1 );
|
|
tv->setParent( win->getContainer() );
|
|
tv->setModel( model );
|
|
tv->expandAll();
|
|
tv->setFocusOnSelection( false );
|
|
|
|
{
|
|
Lock l( mDocCurrentSymbolsMutex );
|
|
auto symbolsIt = mDocCurrentSymbols.find( uri );
|
|
ModelIndex index;
|
|
if ( symbolsIt != mDocCurrentSymbols.end() ) {
|
|
auto docSymbols = symbolsIt->second;
|
|
std::vector<std::string> path;
|
|
path.reserve( docSymbols.size() );
|
|
for ( const auto& sym : docSymbols )
|
|
path.emplace_back( sym.name );
|
|
tv->openRowWithPath( path );
|
|
}
|
|
}
|
|
|
|
win->show();
|
|
input->setFocus();
|
|
|
|
input->on( Event::KeyDown, [tv, input]( const Event* event ) {
|
|
tv->forceKeyDown( *event->asKeyEvent() );
|
|
input->setFocus();
|
|
} );
|
|
|
|
input->on( Event::OnTextChanged, [tv, input, uri, this]( const Event* ) {
|
|
tv->setModel( createDocSymbolsModel( uri, input->getText().toUtf8() ) );
|
|
tv->expandAll();
|
|
} );
|
|
|
|
tv->setOnSelection( [editor]( const ModelIndex& index ) {
|
|
LSPSymbolInformation* node = static_cast<LSPSymbolInformation*>( index.internalData() );
|
|
editor->getDocument().setSelection( node->range.start() );
|
|
editor->scrollTo( node->range.start(), true );
|
|
} );
|
|
|
|
input->on( Event::OnPressEnter,
|
|
[win]( const Event* ) { win->executeKeyBindingCommand( "closeWindow" ); } );
|
|
|
|
tv->on( Event::OnModelEvent, [win]( const Event* event ) {
|
|
const ModelEvent* modelEvent = static_cast<const ModelEvent*>( event );
|
|
if ( modelEvent->getModelEventType() == ModelEventType::Open )
|
|
win->executeKeyBindingCommand( "closeWindow" );
|
|
} );
|
|
}
|
|
|
|
std::shared_ptr<LSPSymbolInfoTreeModel>
|
|
LSPClientPlugin::createDocSymbolsModel( URI uri, const std::string& query ) {
|
|
LSPSymbolInformationList docSymbolsCopy;
|
|
{
|
|
Lock l( mDocSymbolsMutex );
|
|
auto docSymbols = mDocSymbols.find( uri );
|
|
if ( docSymbols == mDocSymbols.end() )
|
|
return nullptr;
|
|
docSymbolsCopy = docSymbols->second;
|
|
}
|
|
return LSPSymbolInfoTreeModel::create( getUISceneNode(), std::move( docSymbolsCopy ), query );
|
|
}
|
|
|
|
const LSPClientServerManager& LSPClientPlugin::getClientManager() const {
|
|
return mClientManager;
|
|
}
|
|
|
|
} // namespace ecode
|