From 3671db4ba699718178578df5ac065260e7fe4326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 5 Nov 2022 03:37:21 -0300 Subject: [PATCH] More LSP implementation work. --- include/eepp/network/uri.hpp | 2 + include/eepp/ui/doc/textrange.hpp | 4 + include/eepp/ui/uicodeeditor.hpp | 4 +- src/eepp/network/uri.cpp | 35 ++ src/eepp/system/process.cpp | 2 +- .../ecode/plugins/linter/linterplugin.cpp | 27 +- .../ecode/plugins/linter/linterplugin.hpp | 1 + .../ecode/plugins/lsp/lspclientplugin.cpp | 59 +++ .../ecode/plugins/lsp/lspclientplugin.hpp | 10 + .../ecode/plugins/lsp/lspclientprotocol.hpp | 107 ++++ .../ecode/plugins/lsp/lspclientserver.cpp | 497 ++++++++++++++---- .../ecode/plugins/lsp/lspclientserver.hpp | 32 ++ .../plugins/lsp/lspclientservermanager.cpp | 20 +- .../plugins/lsp/lspclientservermanager.hpp | 1 + 14 files changed, 686 insertions(+), 115 deletions(-) diff --git a/include/eepp/network/uri.hpp b/include/eepp/network/uri.hpp index b37ed320c..121689d28 100644 --- a/include/eepp/network/uri.hpp +++ b/include/eepp/network/uri.hpp @@ -94,6 +94,8 @@ class EE_API URI { /** Parses and assigns an URI from the given string. */ URI& operator=( const char* uri ); + bool operator<( const URI& url ) const; + /** Swaps the URI with another one. */ void swap( URI& uri ); diff --git a/include/eepp/ui/doc/textrange.hpp b/include/eepp/ui/doc/textrange.hpp index 334b7f8f8..4117fd83e 100644 --- a/include/eepp/ui/doc/textrange.hpp +++ b/include/eepp/ui/doc/textrange.hpp @@ -57,6 +57,10 @@ class EE_API TextRange { return true; } + bool contains( const TextRange& range ) const { + return range.start() >= start() && range.end() <= end(); + } + bool hasSelection() const { return isValid() && mStart != mEnd; } bool inSameLine() const { return isValid() && mStart.line() == mEnd.line(); } diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index 7fee3cbb3..0e1930a85 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -539,6 +539,8 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { void showFindReplace(); + TextPosition resolveScreenPosition( const Vector2f& position, bool clamp = true ) const; + protected: struct LastXOffset { TextPosition position; @@ -716,8 +718,6 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { void resetCursor(); - TextPosition resolveScreenPosition( const Vector2f& position, bool clamp = true ) const; - Vector2f getViewPortLineCount() const; Sizef getMaxScroll() const; diff --git a/src/eepp/network/uri.cpp b/src/eepp/network/uri.cpp index a4e7554a1..6e3d3acc3 100644 --- a/src/eepp/network/uri.cpp +++ b/src/eepp/network/uri.cpp @@ -716,4 +716,39 @@ void URI::buildPath( const std::vector& segments, bool leadingSlash mPath += '/'; } +bool URI::operator<( const URI& url ) const { + int cmp; + cmp = mScheme.compare( url.mScheme ); + if ( mScheme != url.mScheme ) + return cmp < 0; + + cmp = mUserInfo.compare( url.mUserInfo ); + if ( cmp != 0 ) + return cmp < 0; + + cmp = mHost.compare( url.mHost ); + if ( cmp != 0 ) + return cmp < 0; + + if ( mPort != url.mPort ) + return mPort < url.mPort; + + cmp = mPath.compare( url.mPath ); + if ( cmp != 0 ) + return cmp < 0; + + if ( mQuery.empty() != url.mQuery.empty() ) + return !url.mQuery.empty(); + + cmp = mQuery.compare( url.mQuery ); + if ( cmp != 0 ) + return cmp < 0; + + if ( mFragment.empty() != url.mFragment.empty() ) + return !url.mFragment.empty(); + + cmp = mFragment.compare( url.mFragment ); + return cmp < 0; +} + }} // namespace EE::Network diff --git a/src/eepp/system/process.cpp b/src/eepp/system/process.cpp index 3002b1543..7847c64e2 100644 --- a/src/eepp/system/process.cpp +++ b/src/eepp/system/process.cpp @@ -31,11 +31,11 @@ Process::Process( const std::string& command, const Uint32& options, Process::~Process() { mShuttingDown = true; + destroy(); if ( mStdOutThread.joinable() ) mStdOutThread.join(); if ( mStdErrThread.joinable() ) mStdErrThread.join(); - destroy(); eeFree( mProcess ); } diff --git a/src/tools/ecode/plugins/linter/linterplugin.cpp b/src/tools/ecode/plugins/linter/linterplugin.cpp index 70529cdbf..ef88cdf8e 100644 --- a/src/tools/ecode/plugins/linter/linterplugin.cpp +++ b/src/tools/ecode/plugins/linter/linterplugin.cpp @@ -87,10 +87,8 @@ void LinterPlugin::loadLinterConfig( const std::string& path ) { if ( j.contains( "config" ) ) { auto& config = j["config"]; - if ( config.contains( "delay_time" ) ) { - Time time( Time::fromString( config["delay_time"].get() ) ); - setDelayTime( time ); - } + if ( config.contains( "delay_time" ) ) + setDelayTime( Time::fromString( config["delay_time"].get() ) ); } if ( !j.contains( "linters" ) ) @@ -563,27 +561,30 @@ bool LinterPlugin::onMouseMove( UICodeEditor* editor, const Vector2i& pos, const auto& matches = matchIt.second; for ( auto& match : matches ) { if ( match.box[editor].contains( localPos ) ) { - editor->setTooltipText( match.text ); - editor->getTooltip()->setDontAutoHideOnMouseMove( true ); - editor->getTooltip()->setPixelsPosition( Vector2f( pos.x, pos.y ) ); - if ( !editor->getTooltip()->isVisible() ) - editor->runOnMainThread( - [&, editor] { editor->getTooltip()->show(); } ); - return false; + mHoveringMatch = true; + editor->runOnMainThread( [&, editor] { + editor->setTooltipText( match.text ); + editor->getTooltip()->setDontAutoHideOnMouseMove( true ); + editor->getTooltip()->setPixelsPosition( Vector2f( pos.x, pos.y ) ); + if ( !editor->getTooltip()->isVisible() ) + editor->getTooltip()->show(); + } ); + return true; } } } } - if ( editor->getTooltip() && editor->getTooltip()->isVisible() ) { + if ( mHoveringMatch && editor->getTooltip() && editor->getTooltip()->isVisible() ) { editor->setTooltipText( "" ); editor->getTooltip()->hide(); + mHoveringMatch = false; } } return false; } bool LinterPlugin::onMouseLeave( UICodeEditor* editor, const Vector2i&, const Uint32& ) { - if ( editor->getTooltip() && editor->getTooltip()->isVisible() ) { + if ( mHoveringMatch && editor->getTooltip() && editor->getTooltip()->isVisible() ) { editor->setTooltipText( "" ); editor->getTooltip()->hide(); } diff --git a/src/tools/ecode/plugins/linter/linterplugin.hpp b/src/tools/ecode/plugins/linter/linterplugin.hpp index 9f446abe5..bf8aa332a 100644 --- a/src/tools/ecode/plugins/linter/linterplugin.hpp +++ b/src/tools/ecode/plugins/linter/linterplugin.hpp @@ -106,6 +106,7 @@ class LinterPlugin : public UICodeEditorPlugin { bool mReady{ false }; bool mShuttingDown{ false }; + bool mHoveringMatch{ false }; LinterPlugin( const PluginManager* pluginManager ); diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp index 4116f8437..cf44493fb 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include using json = nlohmann::json; @@ -99,6 +100,12 @@ void LSPClientPlugin::loadLSPConfig( std::vector& lsps, const std return; } + if ( j.contains( "config" ) ) { + auto& config = j["config"]; + if ( config.contains( "hover_delay" ) ) + setHoverDelay( Time::fromString( config["hover_delay"].get() ) ); + } + if ( mKeyBindings.empty() ) mKeyBindings["lsp-go-to-definition"] = "f2"; @@ -306,4 +313,56 @@ bool LSPClientPlugin::onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* me return false; } +static void hideTooltip( UICodeEditor* editor ) { + if ( editor->getTooltip() && editor->getTooltip()->isVisible() ) { + editor->setTooltipText( "" ); + editor->getTooltip()->hide(); + } +} + +bool LSPClientPlugin::onMouseMove( UICodeEditor* editor, const Vector2i& position, const Uint32& ) { + Uint32 tag = String::hash( editor->getDocument().getFilePath() ); + editor->removeActionsByTag( tag ); + editor->runOnMainThread( + [&, editor, position]() { + TextPosition cursorPosition = editor->resolveScreenPosition( position.asFloat() ); + mThreadPool->run( [&, editor, position, cursorPosition]() { + mClientManager.getOneLSPClientServer( editor )->documentHover( + editor->getDocument().getURI(), cursorPosition, + [&, editor, position]( const LSPHover& resp ) { + mCurrentHover = resp; + if ( resp.range.isValid() && !resp.contents.empty() ) { + editor->runOnMainThread( [editor, resp, position]() { + editor->setTooltipText( resp.contents[0].value ); + editor->getTooltip()->setHorizontalAlign( UI_HALIGN_LEFT ); + editor->getTooltip()->setPixelsPosition( position.asFloat() ); + editor->getTooltip()->setDontAutoHideOnMouseMove( true ); + if ( editor->hasFocus() && !editor->getTooltip()->isVisible() ) + editor->getTooltip()->show(); + } ); + } + } ); + } ); + }, + mHoverDelay, tag ); + + TextPosition cursorPosition = editor->resolveScreenPosition( position.asFloat() ); + if ( !mCurrentHover.range.isValid() || !mCurrentHover.range.contains( cursorPosition ) ) + hideTooltip( editor ); + return mCurrentHover.range.isValid() && editor->getTooltip() && + editor->getTooltip()->isVisible(); +} + +void LSPClientPlugin::onFocusLoss( UICodeEditor* editor ) { + hideTooltip( editor ); +} + +const Time& LSPClientPlugin::getHoverDelay() const { + return mHoverDelay; +} + +void LSPClientPlugin::setHoverDelay( const Time& hoverDelay ) { + mHoverDelay = hoverDelay; +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp index 882ead3c6..2420c94cc 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp @@ -54,6 +54,13 @@ class LSPClientPlugin : public UICodeEditorPlugin { virtual bool onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* menu, const Vector2i& position, const Uint32& flags ); + virtual bool onMouseMove( UICodeEditor* editor, const Vector2i& position, const Uint32& flags ); + + virtual void onFocusLoss( UICodeEditor* editor ); + + const Time& getHoverDelay() const; + void setHoverDelay( const Time& hoverDelay ); + protected: const PluginManager* mManager{ nullptr }; std::shared_ptr mThreadPool; @@ -68,6 +75,9 @@ class LSPClientPlugin : public UICodeEditorPlugin { bool mReady{ false }; std::map mKeyBindings; /* cmd, shortcut */ std::map> mDelayedDocs; + Uint32 mHoverWaitCb; + LSPHover mCurrentHover; + Time mHoverDelay{ Seconds( 1.f ) }; LSPClientPlugin( const PluginManager* pluginManager ); diff --git a/src/tools/ecode/plugins/lsp/lspclientprotocol.hpp b/src/tools/ecode/plugins/lsp/lspclientprotocol.hpp index 88e4345bf..eff53eb68 100644 --- a/src/tools/ecode/plugins/lsp/lspclientprotocol.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientprotocol.hpp @@ -5,6 +5,7 @@ #include #include +using namespace EE; using namespace EE::UI::Doc; using namespace EE::Network; @@ -189,8 +190,114 @@ template struct LSPProgressParams { T value; }; +enum class LSPSymbolKind { + File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, + Object = 19, + Key = 20, + Null = 21, + EnumMember = 22, + Struct = 23, + Event = 24, + Operator = 25, + TypeParameter = 26, +}; + +enum class LSPSymbolTag : Uint8 { + Deprecated = 1, +}; + +struct LSPSymbolInformation { + LSPSymbolInformation() = default; + LSPSymbolInformation( const std::string& _name, LSPSymbolKind _kind, TextRange _range, + const std::string& _detail ) : + name( _name ), detail( _detail ), kind( _kind ), range( _range ) {} + std::string name; + std::string detail; + LSPSymbolKind kind; + URI url; + TextRange range; + TextRange selectionRange; + double score = 0.0; + LSPSymbolTag tags; + std::vector children; +}; + using LSPWorkDoneProgressParams = LSPProgressParams; +struct LSPResponseError { + LSPErrorCode code{}; + std::string message; + nlohmann::json data; +}; + +enum class LSPMarkupKind { None = 0, PlainText = 1, MarkDown = 2 }; + +struct LSPMarkupContent { + LSPMarkupKind kind = LSPMarkupKind::None; + std::string value; +}; + +struct LSPHover { + std::vector contents; + TextRange range; +}; + + +enum class LSPCompletionItemKind { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +}; + +struct LSPCompletionItem { + std::string label; + LSPCompletionItemKind kind; + std::string detail; + LSPMarkupContent documentation; + std::string sortText; + std::string insertText; + std::vector additionalTextEdits; +}; + } // namespace ecode #endif // ECODE_LSPCLIENTPROTOCOL_HPP diff --git a/src/tools/ecode/plugins/lsp/lspclientserver.cpp b/src/tools/ecode/plugins/lsp/lspclientserver.cpp index 3f7d48fd6..5968444f5 100644 --- a/src/tools/ecode/plugins/lsp/lspclientserver.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientserver.cpp @@ -32,14 +32,14 @@ static const char* MEMBER_LOCATION = "location"; static const char* MEMBER_RANGE = "range"; static const char* MEMBER_LINE = "line"; static const char* MEMBER_CHARACTER = "character"; -// static const char* MEMBER_KIND = "kind"; -// static const char* MEMBER_LABEL = "label"; -// static const char* MEMBER_DOCUMENTATION = "documentation"; -// static const char* MEMBER_DETAIL = "detail"; -// static const char* MEMBER_COMMAND = "command"; -// static const char* MEMBER_EDIT = "edit"; -// static const char* MEMBER_TITLE = "title"; -// static const char* MEMBER_ARGUMENTS = "arguments"; +static const char* MEMBER_KIND = "kind"; +static const char* MEMBER_LABEL = "label"; +static const char* MEMBER_DOCUMENTATION = "documentation"; +static const char* MEMBER_DETAIL = "detail"; +static const char* MEMBER_COMMAND = "command"; +static const char* MEMBER_EDIT = "edit"; +static const char* MEMBER_TITLE = "title"; +static const char* MEMBER_ARGUMENTS = "arguments"; static const char* MEMBER_DIAGNOSTICS = "diagnostics"; static const char* MEMBER_TARGET_URI = "targetUri"; static const char* MEMBER_TARGET_RANGE = "targetRange"; @@ -295,14 +295,87 @@ static std::vector parseDiagnosticsArr( const json& result ) { return ret; } -static LSPPublishDiagnosticsParams parseDiagnostics( const json& result ) { +static LSPPublishDiagnosticsParams parsePublishDiagnostics( const json& result ) { LSPPublishDiagnosticsParams ret; ret.uri = URI( result.at( MEMBER_URI ).get() ); ret.diagnostics = parseDiagnosticsArr( result.at( MEMBER_DIAGNOSTICS ) ); return ret; } -/*static std::vector parseTextEdit( const json& result ) { +static bool isPositionValid( const TextPosition& pos ) { + return pos.column() >= 0 && pos.line() >= 0; +} + +static std::vector parseDocumentSymbols( const json& result ) { + // the reply could be old SymbolInformation[] or new (hierarchical) DocumentSymbol[] + // try to parse it adaptively in any case + // if new style, hierarchy is specified clearly in reply + // if old style, it is assumed the values enter linearly, that is; + // * a parent/container is listed before its children + // * if a name is defined/declared several times and then used as a parent, + // then we try to find such a parent whose range contains current range + // (otherwise fall back to using the last instance as a parent) + std::vector ret; + std::map index; + + std::function parseSymbol = + [&]( const json& symbol, LSPSymbolInformation* parent ) { + const auto& mrange = symbol.contains( MEMBER_RANGE ) + ? symbol.at( MEMBER_RANGE ) + : symbol[MEMBER_LOCATION].at( MEMBER_RANGE ); + auto range = parseRange( mrange ); + std::map::iterator it = index.end(); + // if flat list, try to find parent by name + if ( !parent ) { + auto container = symbol.value( "containerName", "" ); + it = index.find( container ); + // default to last inserted + if ( it != index.end() ) { + parent = it->second; + } + // but prefer a containing range + while ( it != index.end() && it->first == container ) { + if ( it->second->range.contains( range ) ) { + parent = it->second; + break; + } + ++it; + } + } + auto list = parent ? &parent->children : &ret; + if ( isPositionValid( range.start() ) && isPositionValid( range.end() ) ) { + auto name = symbol.at( ( "name" ) ).get(); + auto kind = static_cast( symbol.at( MEMBER_KIND ).get() ); + auto detail = symbol.value( MEMBER_DETAIL, "" ); + list->push_back( { name, kind, range, detail } ); + index.insert( std::pair( name, &list->back() ) ); + // proceed recursively + if ( symbol.contains( "children" ) ) { + const auto& children = symbol.at( ( "children" ) ); + for ( const auto& child : children ) + parseSymbol( child, &list->back() ); + } + } + }; + + const auto symInfos = result; + for ( const auto& info : symInfos ) + parseSymbol( info, nullptr ); + return ret; +} + +static LSPResponseError parseResponseError( const json& v ) { + LSPResponseError ret; + if ( v.is_object() ) { + const auto& vm = v; + ret.code = LSPErrorCode( vm.at( MEMBER_CODE ).get() ); + ret.message = vm.at( MEMBER_MESSAGE ).get(); + ret.data = vm.value( "data", json() ); + } + return ret; +} + +static std::vector parseTextEdit( const json& result ) { std::vector ret; const auto textEdits = result; for ( const auto& redit : textEdits ) { @@ -352,20 +425,56 @@ static LSPCommand parseCommand( const json& result ) { return { title, command, args }; } +static std::vector parseDiagnostics( const json& result ) { + std::vector ret; + for ( const auto& vdiag : result ) { + const auto& diag = vdiag; + auto range = parseRange( diag.at( MEMBER_RANGE ) ); + auto severity = static_cast( diag.value( "severity", 0 ) ); + std::string code; + if ( diag.contains( "code" ) ) { + if ( diag["code"].is_number_integer() ) + code = String::toString( diag["code"].get() ); + else + code = diag.value( "code", "" ); + } + + auto source = diag.value( "source", "" ); + auto message = diag.value( MEMBER_MESSAGE, "" ); + std::vector relatedInfoList; + if ( diag.contains( "relatedInformation" ) ) { + const auto& relatedInfo = diag.at( "relatedInformation" ); + for ( const auto& related : relatedInfo ) { + auto relLocation = parseLocation( related.at( MEMBER_LOCATION ) ); + auto relMessage = related.value( MEMBER_MESSAGE, "" ); + relatedInfoList.push_back( { relLocation, relMessage } ); + } + } + ret.push_back( { range, severity, code, source, message, relatedInfoList } ); + } + return ret; +} + static std::vector parseCodeAction( const json& result ) { std::vector ret; - const auto codeActions = result; + const auto& codeActions = result; for ( const auto& vaction : codeActions ) { - auto action = vaction; + auto& action = vaction; // entry could be Command or CodeAction if ( !action.at( MEMBER_COMMAND ).is_string() ) { // CodeAction auto title = action.at( MEMBER_TITLE ).get(); - auto kind = action.at( MEMBER_KIND ).get(); - auto command = parseCommand( action.at( MEMBER_COMMAND ) ); - auto edit = parseWorkSpaceEdit( action.at( MEMBER_EDIT ) ); - auto diagnostics = parseDiagnostics( action.at( MEMBER_DIAGNOSTICS ) ); - ret.push_back( { title, kind, diagnostics, edit, command } ); + auto kind = action.value( MEMBER_KIND, "" ); + auto command = action.contains( MEMBER_COMMAND ) + ? parseCommand( action.at( MEMBER_COMMAND ) ) + : LSPCommand{}; + auto edit = action.at( MEMBER_EDIT ) ? parseWorkSpaceEdit( action.at( MEMBER_EDIT ) ) + : LSPWorkspaceEdit{}; + auto diagnostics = action.contains( MEMBER_DIAGNOSTICS ) + ? parseDiagnostics( action.at( MEMBER_DIAGNOSTICS ) ) + : std::vector{}; + LSPCodeAction action = { title, kind, diagnostics, edit, command }; + ret.push_back( action ); } else { // Command auto command = parseCommand( action ); @@ -373,7 +482,212 @@ static std::vector parseCodeAction( const json& result ) { } } return ret; -}*/ +} + +static json toJson( const LSPLocation& location ) { + if ( !location.uri.empty() ) { + return json{ { MEMBER_URI, location.uri.toString() }, + { MEMBER_RANGE, toJson( location.range ) } }; + } + return json(); +} + +static json toJson( const LSPDiagnosticRelatedInformation& related ) { + auto loc = toJson( related.location ); + if ( loc.is_object() ) { + return json{ { MEMBER_LOCATION, toJson( related.location ) }, + { MEMBER_MESSAGE, related.message } }; + } + return json(); +} + +static json toJson( const LSPDiagnostic& diagnostic ) { + // required + auto result = json(); + result[MEMBER_RANGE] = toJson( diagnostic.range ); + result[MEMBER_MESSAGE] = diagnostic.message; + // optional + if ( !diagnostic.code.empty() ) + result[( "code" )] = diagnostic.code; + if ( diagnostic.severity != LSPDiagnosticSeverity::Unknown ) + result[( "severity" )] = static_cast( diagnostic.severity ); + if ( !diagnostic.source.empty() ) + result[( "source" )] = diagnostic.source; + json relatedInfo; + for ( const auto& vrelated : diagnostic.relatedInformation ) { + auto related = toJson( vrelated ); + if ( related.is_object() ) { + relatedInfo.push_back( related ); + } + } + result[( "relatedInformation" )] = relatedInfo; + return result; +} + +static json codeActionParams( const URI& document, const TextRange& range, + const std::vector& kinds, + const std::vector& diagnostics ) { + auto params = textDocumentParams( document ); + params[MEMBER_RANGE] = toJson( range ); + json context; + json diags; + for ( const auto& diagnostic : diagnostics ) { + diags.push_back( toJson( diagnostic ) ); + } + context[MEMBER_DIAGNOSTICS] = diags; + if ( !kinds.empty() ) + context["only"] = json( kinds ); + params["context"] = context; + return params; +} + +static LSPMarkupContent parseMarkupContent( const json& v ) { + LSPMarkupContent ret; + if ( v.is_object() ) { + ret.value = v.at( "value" ); + auto kind = v.value( MEMBER_KIND, "plaintext" ); + if ( kind == "plaintext" ) { + ret.kind = LSPMarkupKind::PlainText; + } else if ( kind == "markdown" ) { + ret.kind = LSPMarkupKind::MarkDown; + } + } else if ( v.is_string() ) { + ret.kind = LSPMarkupKind::PlainText; + ret.value = v.get(); + } + return ret; +} + +static LSPHover parseHover( const json& result ) { + LSPHover ret; + if ( result.is_null() ) + return ret; + + if ( result.contains( MEMBER_RANGE ) ) + ret.range = parseRange( result.at( MEMBER_RANGE ) ); + const auto& contents = result.at( "contents" ); + + if ( contents.is_array() ) { + for ( const auto& c : contents ) + ret.contents.push_back( parseMarkupContent( c ) ); + } else { + ret.contents.push_back( parseMarkupContent( contents ) ); + } + + return ret; +} + +static std::vector supportedSemanticTokenTypes() { + return { "namespace", "type", "class", "enum", "interface", "struct", + "typeParameter", "parameter", "variable", "property", "enumMember", "event", + "function", "method", "macro", "keyword", "modifier", "comment", + "string", "number", "regexp", "operator" }; +} + +static std::vector parseDocumentCompletion( const json& result ) { + std::vector ret; + if ( result.empty() ) + return {}; + const json& items = + ( result.is_object() && result.contains( "items" ) ) ? result["items"] : result; + + for ( const auto& item : items ) { + auto label = item.value( MEMBER_LABEL, "" ); + auto detail = item.value( MEMBER_DETAIL, "" ); + LSPMarkupContent doc = item.contains( MEMBER_DOCUMENTATION ) + ? parseMarkupContent( item.at( MEMBER_DOCUMENTATION ) ) + : LSPMarkupContent{}; + auto sortText = item.value( "sortText", "" ); + if ( sortText.empty() ) + sortText = label; + auto insertText = item.value( "insertText", "" ); + if ( insertText.empty() ) + insertText = label; + if ( item.contains( "textEdit" ) ) { + const auto& textEdit = item["textEdit"]; + if ( !textEdit.empty() ) { + auto newText = textEdit.value( "newText", "" ); + insertText = newText; + } + } + auto kind = static_cast( item.value( MEMBER_KIND, 1 ) ); + + const std::vector additionalTextEdits = + item.contains( "additionalTextEdits" ) + ? parseTextEdit( item.at( "additionalTextEdits" ) ) + : std::vector{}; + + ret.push_back( { label, kind, detail, doc, sortText, insertText, + additionalTextEdits /*, textEdit*/ } ); + } + return ret; +} + +void LSPClientServer::initialize() { + json codeAction{ + { "codeActionLiteralSupport", json{ { "codeActionKind", json{ { "valueSet", {} } } } } } }; + + json semanticTokens{ + { "requests", json{ { "range", true }, { "full", json{ { "delta", true } } } } }, + { "tokenTypes", supportedSemanticTokenTypes() }, + { "tokenModifiers", {} }, + { "formats", { "relative" } }, + }; + + json capabilities{ + { + "textDocument", + json{ { "documentSymbol", json{ { "hierarchicalDocumentSymbolSupport", true } } }, + { "publishDiagnostics", json{ { "relatedInformation", true } } }, + { "codeAction", codeAction }, + { "semanticTokens", semanticTokens }, + { "synchronization", json{ { "didSave", true } } }, + { "selectionRange", json{ { "dynamicRegistration", false } } }, + { "hover", json{ { "contentFormat", { "plaintext" } } } } }, + }, + { "window", json{ { "workDoneProgress", true } } }, + { "general", json{ { "positionEncodings", json::array( { "utf-32" } ) } } } }; + + json params{ { "processId", Sys::getProcessID() }, + { "capabilities", capabilities }, + { "initializationOptions", {} } }; + + std::string rootPath = mRootPath; + if ( rootPath.empty() ) { + if ( !mManager->getLSPWorkspaceFolder().uri.empty() ) + rootPath = mManager->getLSPWorkspaceFolder().uri.getPath(); + else + rootPath = FileSystem::getCurrentWorkingDirectory(); + } + + std::string uriRootPath = "file://" + rootPath; + params["rootPath"] = rootPath; + params["rootUri"] = uriRootPath; + params["workspaceFolders"] = + toJson( { LSPWorkspaceFolder{ uriRootPath, FileSystem::fileNameFromPath( rootPath ) } } ); + capabilities["workspace"] = json{ { "workspaceFolders", true }, { "configuration", false } }; + + write( + newRequest( "initialize", params ), + [&]( const json& resp ) { +#ifndef EE_DEBUG + try { +#endif + fromJson( mCapabilities, resp["capabilities"] ); +#ifndef EE_DEBUG + } catch ( const json::exception& e ) { + Log::warning( + "LSPClientServer::initialize server %s error parsing capabilities: %s", + mLSP.name.c_str(), e.what() ); + } +#endif + + mReady = true; + write( newRequest( "initialized" ) ); + sendQueuedMessages(); + }, + [&]( const json& ) {} ); +} LSPClientServer::LSPClientServer( LSPClientServerManager* manager, const String::HashType& id, const LSPDefinition& lsp, const std::string& rootPath ) : @@ -422,6 +736,7 @@ const LSPServerCapabilities& LSPClientServer::getCapabilities() const { } LSPClientServer::RequestHandle LSPClientServer::cancel( int reqid ) { + Lock l( mHandlersMutex ); if ( mHandlers.erase( reqid ) > 0 ) { auto params = json{ MEMBER_ID, reqid }; return write( newRequest( "$/cancelRequest", params ) ); @@ -444,6 +759,7 @@ LSPClientServer::RequestHandle LSPClientServer::write( const json& msg, const Js if ( h ) { ob[MEMBER_ID] = ++mLastMsgId; ret.mId = mLastMsgId; + Lock l( mHandlersMutex ); mHandlers[mLastMsgId] = { h, eh }; } else if ( id ) { ob[MEMBER_ID] = id; @@ -584,6 +900,22 @@ LSPClientServer::RequestHandle LSPClientServer::documentSymbols( const URI& docu return send( newRequest( "textDocument/documentSymbol", params ), h, eh ); } +LSPClientServer::RequestHandle +LSPClientServer::documentSymbols( const URI& document, + const ReplyHandler>& h, + const ReplyHandler& eh ) { + return documentSymbols( + document, + [h]( const json& json ) { + if ( h ) + h( parseDocumentSymbols( json ) ); + }, + [eh]( const json& json ) { + if ( eh ) + eh( parseResponseError( json ) ); + } ); +} + void fromJson( LSPWorkDoneProgressValue& value, const json& json ) { if ( !json.empty() ) { auto ob = json; @@ -620,7 +952,7 @@ static json newError( const LSPErrorCode& code, const std::string& msg ) { void LSPClientServer::publishDiagnostics( const json& msg ) { // should emmit event somewhere - auto res = parseDiagnostics( msg[MEMBER_PARAMS] ); + auto res = parsePublishDiagnostics( msg[MEMBER_PARAMS] ); Log::debug( "LSPClientServer::publishDiagnostics: %s - returned %zu items", res.uri.toString().c_str(), res.diagnostics.size() ); } @@ -732,6 +1064,7 @@ void LSPClientServer::readStdOut( const char* bytes, size_t n ) { Log::debug( "LSPClientServer::readStdOut server %s said: \n%s", mLSP.name.c_str(), res.dump().c_str() ); + Lock l( mHandlersMutex ); auto it = mHandlers.find( msgid ); if ( it != mHandlers.end() ) { const auto handler = *it; @@ -771,79 +1104,6 @@ void LSPClientServer::readStdErr( const char* bytes, size_t n ) { } } -static std::vector supportedSemanticTokenTypes() { - return { "namespace", "type", "class", "enum", "interface", "struct", - "typeParameter", "parameter", "variable", "property", "enumMember", "event", - "function", "method", "macro", "keyword", "modifier", "comment", - "string", "number", "regexp", "operator" }; -} - -void LSPClientServer::initialize() { - json codeAction{ - { "codeActionLiteralSupport", json{ { "codeActionKind", json{ { "valueSet", {} } } } } } }; - - json semanticTokens{ - { "requests", json{ { "range", true }, { "full", json{ { "delta", true } } } } }, - { "tokenTypes", supportedSemanticTokenTypes() }, - { "tokenModifiers", {} }, - { "formats", { "relative" } }, - }; - - json capabilities{ - { - "textDocument", - json{ { "documentSymbol", json{ { "hierarchicalDocumentSymbolSupport", true } } }, - { "publishDiagnostics", json{ { "relatedInformation", true } } }, - { "codeAction", codeAction }, - { "semanticTokens", semanticTokens }, - { "synchronization", json{ { "didSave", true } } }, - { "selectionRange", json{ { "dynamicRegistration", false } } }, - { "hover", json{ { "contentFormat", { "plaintext" } } } } }, - }, - { "window", json{ { "workDoneProgress", true } } }, - { "general", json{ { "positionEncodings", json::array( { "utf-32" } ) } } } }; - - json params{ { "processId", Sys::getProcessID() }, - { "capabilities", capabilities }, - { "initializationOptions", {} } }; - - std::string rootPath = mRootPath; - if ( rootPath.empty() ) { - if ( !mManager->getLSPWorkspaceFolder().uri.empty() ) - rootPath = mManager->getLSPWorkspaceFolder().uri.getPath(); - else - rootPath = FileSystem::getCurrentWorkingDirectory(); - } - - std::string uriRootPath = "file://" + rootPath; - params["rootPath"] = rootPath; - params["rootUri"] = uriRootPath; - params["workspaceFolders"] = - toJson( { LSPWorkspaceFolder{ uriRootPath, FileSystem::fileNameFromPath( rootPath ) } } ); - capabilities["workspace"] = json{ { "workspaceFolders", true }, { "configuration", false } }; - - write( - newRequest( "initialize", params ), - [&]( const json& resp ) { -#ifndef EE_DEBUG - try { -#endif - fromJson( mCapabilities, resp["capabilities"] ); -#ifndef EE_DEBUG - } catch ( const json::exception& e ) { - Log::warning( - "LSPClientServer::initialize server %s error parsing capabilities: %s", - mLSP.name.c_str(), e.what() ); - } -#endif - - mReady = true; - write( newRequest( "initialized" ) ); - sendQueuedMessages(); - }, - [&]( const json& ) {} ); -} - void LSPClientServer::sendQueuedMessages() { for ( const auto& msg : mQueuedMessages ) write( msg.msg, msg.h, msg.eh ); @@ -900,4 +1160,57 @@ LSPClientServer::didChangeWorkspaceFolders( const std::vector& kinds, + std::vector diagnostics, const JsonReplyHandler& h ) { + auto params = codeActionParams( document, range, kinds, std::move( diagnostics ) ); + return send( newRequest( "textDocument/codeAction", params ), h ); +} + +LSPClientServer::RequestHandle LSPClientServer::documentCodeAction( + const URI& document, const TextRange& range, const std::vector& kinds, + std::vector diagnostics, const CodeActionHandler& h ) { + return documentCodeAction( document, range, kinds, diagnostics, [h]( const json& json ) { + if ( h ) + h( parseCodeAction( json ) ); + } ); +} + +LSPClientServer::RequestHandle LSPClientServer::documentHover( const URI& document, + const TextPosition& pos, + const JsonReplyHandler& h ) { + auto params = textDocumentPositionParams( document, pos ); + return send( newRequest( "textDocument/hover", params ), h ); +} + +LSPClientServer::RequestHandle LSPClientServer::documentHover( const URI& document, + const TextPosition& pos, + const HoverHandler& h ) { + return documentHover( document, pos, [h]( const json& json ) { + if ( h ) + h( parseHover( json ) ); + } ); +} + +LSPClientServer::RequestHandle LSPClientServer::documentHover( TextDocument* doc, + const HoverHandler& h ) { + return documentHover( doc->getURI(), doc->getSelection().start(), h ); +} + +LSPClientServer::RequestHandle LSPClientServer::documentCompletion( const URI& document, + const TextPosition& pos, + const JsonReplyHandler& h ) { + auto params = textDocumentPositionParams( document, pos ); + return send( newRequest( "textDocument/completion", params ), h ); +} + +LSPClientServer::RequestHandle LSPClientServer::documentCompletion( const URI& document, + const TextPosition& pos, + const CompletionHandler& h ) { + return documentCompletion( document, pos, [h]( const json& json ) { + if ( h ) + h( parseDocumentCompletion( json ) ); + } ); +} + } // namespace ecode diff --git a/src/tools/ecode/plugins/lsp/lspclientserver.hpp b/src/tools/ecode/plugins/lsp/lspclientserver.hpp index efc3ddef6..19f4c6623 100644 --- a/src/tools/ecode/plugins/lsp/lspclientserver.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientserver.hpp @@ -28,6 +28,9 @@ class LSPClientServer { template using ReplyHandler = std::function; using JsonReplyHandler = ReplyHandler; + using CodeActionHandler = ReplyHandler>; + using HoverHandler = ReplyHandler; + using CompletionHandler = ReplyHandler>; class RequestHandle { friend class LSPClientServer; @@ -67,6 +70,10 @@ class LSPClientServer { LSPClientServer::RequestHandle documentSymbols( const URI& document, const JsonReplyHandler& h, const JsonReplyHandler& eh ); + LSPClientServer::RequestHandle + documentSymbols( const URI& document, const ReplyHandler>& h, + const ReplyHandler& eh ); + LSPClientServer::RequestHandle didOpen( const URI& document, const std::string& text, int version ); @@ -118,6 +125,30 @@ class LSPClientServer { LSPClientServer::RequestHandle switchSourceHeader( const URI& document ); + LSPClientServer::RequestHandle documentCodeAction( const URI& document, const TextRange& range, + const std::vector& kinds, + std::vector diagnostics, + const JsonReplyHandler& h ); + + LSPClientServer::RequestHandle documentCodeAction( const URI& document, const TextRange& range, + const std::vector& kinds, + std::vector diagnostics, + const CodeActionHandler& h ); + + LSPClientServer::RequestHandle documentHover( const URI& document, const TextPosition& pos, + const JsonReplyHandler& h ); + + LSPClientServer::RequestHandle documentHover( const URI& document, const TextPosition& pos, + const HoverHandler& h ); + + LSPClientServer::RequestHandle documentHover( TextDocument* doc, const HoverHandler& h ); + + LSPClientServer::RequestHandle documentCompletion( const URI& document, const TextPosition& pos, + const JsonReplyHandler& h ); + + LSPClientServer::RequestHandle documentCompletion( const URI& document, const TextPosition& pos, + const CompletionHandler& h ); + protected: LSPClientServerManager* mManager{ nullptr }; String::HashType mId; @@ -128,6 +159,7 @@ class LSPClientServer { std::map> mClients; std::map> mHandlers; Mutex mClientsMutex; + Mutex mHandlersMutex; bool mReady{ false }; struct QueueMessage { json msg; diff --git a/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp b/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp index 647ef0635..023e5b678 100644 --- a/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp @@ -78,6 +78,7 @@ void LSPClientServerManager::tryRunServer( const std::shared_ptr& auto rootPath = findRootPath( lsp, doc ); auto lspName = lsp.name.empty() ? lsp.command : lsp.name; String::HashType id = String::hash( lspName + "|" + lsp.language + "|" + rootPath ); + Lock l( mClientsMutex ); auto clientIt = mClients.find( id ); LSPClientServer* server = nullptr; if ( clientIt == mClients.end() ) { @@ -97,6 +98,7 @@ void LSPClientServerManager::tryRunServer( const std::shared_ptr& void LSPClientServerManager::closeLSPServer( const String::HashType& id ) { mThreadPool->run( [this, id]() { + Lock l( mClientsMutex ); auto it = mClients.find( id ); if ( it != mClients.end() ) { mClients.erase( it ); @@ -145,13 +147,15 @@ const std::shared_ptr& LSPClientServerManager::getThreadPool() const } void LSPClientServerManager::updateDirty() { - for ( auto& server : mClients ) { - server.second->updateDirty(); + { + Lock l( mClientsMutex ); + for ( auto& server : mClients ) { + server.second->updateDirty(); - if ( !server.second->hasDocuments() ) - mLSPsToClose.push_back( server.first ); + if ( !server.second->hasDocuments() ) + mLSPsToClose.push_back( server.first ); + } } - if ( !mLSPsToClose.empty() ) for ( const auto& server : mLSPsToClose ) closeLSPServer( server ); @@ -166,9 +170,9 @@ void LSPClientServerManager::getAndGoToLocation( const std::shared_ptrdidChangeWorkspaceFolders( { mLSPWorkspaceFolder }, {} ); - } } const LSPWorkspaceFolder& LSPClientServerManager::getLSPWorkspaceFolder() const { @@ -182,6 +186,7 @@ std::vector LSPClientServerManager::getLSPClientServers( UICod std::vector LSPClientServerManager::getLSPClientServers( const std::shared_ptr& doc ) { std::vector servers; + Lock l( mClientsMutex ); for ( auto& server : mClients ) { if ( server.second->hasDocument( doc.get() ) ) servers.push_back( server.second.get() ); @@ -195,6 +200,7 @@ LSPClientServer* LSPClientServerManager::getOneLSPClientServer( UICodeEditor* ed LSPClientServer* LSPClientServerManager::getOneLSPClientServer( const std::shared_ptr& doc ) { + Lock l( mClientsMutex ); for ( auto& server : mClients ) { if ( server.second->hasDocument( doc.get() ) ) return server.second.get(); diff --git a/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp b/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp index 1517302cf..a1511ed4b 100644 --- a/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp @@ -56,6 +56,7 @@ class LSPClientServerManager { std::vector mLSPs; std::vector mLSPsToClose; LSPWorkspaceFolder mLSPWorkspaceFolder; + Mutex mClientsMutex; std::vector supportsLSP( const std::shared_ptr& doc );