From 3db37b15b2d12f4a1e697288afe0b883b1fa8c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 20 Feb 2023 20:02:52 -0300 Subject: [PATCH] ecode: LSP - Added code action support. Fixed issues with rust-analyzer. --- include/eepp/math/rect.hpp | 43 ++- include/eepp/ui/doc/textdocument.hpp | 5 + .../eepp/ui/tools/uicodeeditorsplitter.hpp | 2 + src/eepp/ui/doc/textdocument.cpp | 12 + src/eepp/ui/tools/uicodeeditorsplitter.cpp | 4 + src/eepp/ui/uicodeeditor.cpp | 6 +- src/tools/ecode/ecode.cpp | 12 +- .../plugins/formatter/formatterplugin.cpp | 8 +- .../plugins/formatter/formatterplugin.hpp | 2 +- .../ecode/plugins/lsp/lspclientplugin.cpp | 332 ++++++++++++------ .../ecode/plugins/lsp/lspclientplugin.hpp | 16 +- .../ecode/plugins/lsp/lspclientserver.cpp | 70 +++- .../plugins/lsp/lspclientservermanager.cpp | 53 ++- .../plugins/lsp/lspclientservermanager.hpp | 9 + src/tools/ecode/plugins/lsp/lspprotocol.hpp | 11 + 15 files changed, 426 insertions(+), 159 deletions(-) diff --git a/include/eepp/math/rect.hpp b/include/eepp/math/rect.hpp index f001ff434..dde20d2cf 100644 --- a/include/eepp/math/rect.hpp +++ b/include/eepp/math/rect.hpp @@ -18,7 +18,7 @@ template class tRECT { tRECT copy() const; - void setPosition( const Vector2& pos ); + tRECT& setPosition( const Vector2& pos ); bool intersect( const tRECT& rect ) const; @@ -26,11 +26,11 @@ template class tRECT { bool contains( const Vector2& Vect ) const; - void expand( const tRECT& rect ); + tRECT& expand( const tRECT& rect ); - void shrink( const tRECT& rect ); + tRECT& shrink( const tRECT& rect ); - void expand( const Vector2& Vect ); + tRECT& expand( const Vector2& Vect ); T area() const; @@ -75,13 +75,13 @@ template class tRECT { tRECT roundDown() const; - void scale( T scale, const Vector2& center ); + tRECT& scale( T scale, const Vector2& center ); - void scale( T scale ); + tRECT& scale( T scale ); - void scale( Vector2 scale, const Vector2& center ); + tRECT& scale( Vector2 scale, const Vector2& center ); - void scale( Vector2 scale ); + tRECT& scale( Vector2 scale ); tRECT asFloat() const; @@ -257,33 +257,37 @@ template T tRECT::getHeight() const { return eeabs( Bottom - Top ); } -template void tRECT::setPosition( const Vector2& pos ) { +template tRECT& tRECT::setPosition( const Vector2& pos ) { auto size = getSize(); Left = pos.x; Bottom = pos.y + size.y; Right = pos.x + size.x; Top = pos.y; + return *this; } -template void tRECT::expand( const tRECT& rect ) { +template tRECT& tRECT::expand( const tRECT& rect ) { Left = eemin( Left, rect.Left ); Bottom = eemax( Bottom, rect.Bottom ); Right = eemax( Right, rect.Right ); Top = eemin( Top, rect.Top ); + return *this; } -template void tRECT::shrink( const tRECT& rect ) { +template tRECT& tRECT::shrink( const tRECT& rect ) { Left = eemax( Left, rect.Left ); Top = eemax( Top, rect.Top ); Right = eemax( Left, eemin( Right, rect.Right ) ); Bottom = eemax( Top, eemin( Bottom, rect.Bottom ) ); + return *this; } -template void tRECT::expand( const Vector2& Vect ) { +template tRECT& tRECT::expand( const Vector2& Vect ) { Left = eemin( Left, Vect.x ); Bottom = eemax( Bottom, Vect.y ); Right = eemax( Right, Vect.x ); Top = eemin( Top, Vect.y ); + return *this; } template T tRECT::area() const { @@ -362,25 +366,26 @@ template Vector2 tRECT::wrapVector( const Vector2& Vect ) return Vector2( x + Left, y + Top ); } -template void tRECT::scale( Vector2 scale, const Vector2& center ) { +template tRECT& tRECT::scale( Vector2 scale, const Vector2& center ) { if ( scale != 1.0f ) { Left = center.x + ( Left - center.x ) * scale.x; Top = center.y + ( Top - center.y ) * scale.y; Right = center.x + ( Right - center.x ) * scale.x; Bottom = center.y + ( Bottom - center.y ) * scale.y; } + return *this; } -template void tRECT::scale( T scale, const Vector2& center ) { - scale( Vector2f( scale, scale ), center ); +template tRECT& tRECT::scale( T scale, const Vector2& center ) { + return scale( Vector2f( scale, scale ), center ); } -template void tRECT::scale( T scale ) { - scale( scale, getCenter() ); +template tRECT& tRECT::scale( T scale ) { + return scale( scale, getCenter() ); } -template void tRECT::scale( Vector2 scale ) { - scale( scale, getCenter() ); +template tRECT& tRECT::scale( Vector2 scale ) { + return scale( scale, getCenter() ); } template tRECT tRECT::ceil() const { diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index e9d26f252..97f329db0 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -542,6 +542,10 @@ class EE_API TextDocument { std::vector getCommandList() const; + bool isRunningTransaction() const; + + void setRunningTransaction( const bool runningTransaction ); + protected: friend class UndoStack; @@ -555,6 +559,7 @@ class EE_API TextDocument { Mutex mClientsMutex; LineEnding mLineEnding{ LineEnding::LF }; std::atomic mLoading{ false }; + std::atomic mRunningTransaction{ false }; std::atomic mLoadingAsync{ false }; bool mIsBOM{ false }; bool mAutoDetectIndentType{ true }; diff --git a/include/eepp/ui/tools/uicodeeditorsplitter.hpp b/include/eepp/ui/tools/uicodeeditorsplitter.hpp index 345048202..6526f6b1b 100644 --- a/include/eepp/ui/tools/uicodeeditorsplitter.hpp +++ b/include/eepp/ui/tools/uicodeeditorsplitter.hpp @@ -191,6 +191,8 @@ class EE_API UICodeEditorSplitter { bool curWidgetExists() const; + bool isCurEditor( UICodeEditor* editor ); + UICodeEditor* getSomeEditor(); size_t countEditorsOpeningDoc( const TextDocument& doc ) const; diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 357a5b120..cfb7aa4ee 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -800,6 +800,14 @@ std::vector TextDocument::getCommandList() const { return cmds; } +bool TextDocument::isRunningTransaction() const { + return mRunningTransaction; +} + +void TextDocument::setRunningTransaction( const bool runningTransaction ) { + mRunningTransaction = runningTransaction; +} + String TextDocument::getSelectedText() const { return getText( getSelection() ); } @@ -1877,12 +1885,16 @@ void TextDocument::setIndentType( const IndentType& indentType ) { } void TextDocument::undo() { + setRunningTransaction( true ); mUndoStack.undo(); + setRunningTransaction( false ); notifyUndoRedo( UndoRedo::Undo ); } void TextDocument::redo() { + setRunningTransaction( true ); mUndoStack.redo(); + setRunningTransaction( false ); notifyUndoRedo( UndoRedo::Redo ); } diff --git a/src/eepp/ui/tools/uicodeeditorsplitter.cpp b/src/eepp/ui/tools/uicodeeditorsplitter.cpp index 314823df9..d63c99b4e 100644 --- a/src/eepp/ui/tools/uicodeeditorsplitter.cpp +++ b/src/eepp/ui/tools/uicodeeditorsplitter.cpp @@ -1062,6 +1062,10 @@ bool UICodeEditorSplitter::curWidgetExists() const { return found || mCurWidget == nullptr; } +bool UICodeEditorSplitter::isCurEditor( UICodeEditor* editor ) { + return mCurEditor == editor; +} + UICodeEditor* UICodeEditorSplitter::getSomeEditor() { UICodeEditor* ed = nullptr; forEachEditorStoppable( [&]( UICodeEditor* editor ) { diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 29979d69e..27b9c0a20 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -386,7 +386,8 @@ void UICodeEditor::scheduledUpdate( const Time& ) { } void UICodeEditor::updateLongestLineWidth() { - if ( mHorizontalScrollBarEnabled && mDoc && !mDoc->isLoading() ) { + if ( mHorizontalScrollBarEnabled && mDoc && !mDoc->isLoading() && + !mDoc->isRunningTransaction() ) { Float maxWidth = mLongestLineWidth; findLongestLine(); mLongestLineWidthLastUpdate.restart(); @@ -2213,9 +2214,8 @@ Int64 UICodeEditor::getColFromXOffset( Int64 lineNumber, const Float& x ) const Float xOffset = 0; Float tabWidth = glyphWidth * mTabWidth; Float hTab = tabWidth * 0.5f; - bool isTab; for ( int i = 0; i < len; i++ ) { - isTab = ( line[i] == '\t' ); + bool isTab = ( line[i] == '\t' ); if ( xOffset >= x ) { return xOffset - x > ( isTab ? hTab : glyphWidth * 0.5f ) ? eemax( 0, i - 1 ) : i; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index baff29b30..ed298329b 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -3105,7 +3105,7 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { backward::SignalHandling sh; #endif args::ArgumentParser parser( "ecode" ); - args::HelpFlag help( parser, "help", "Display this help menu", { 'h', "help" } ); + args::HelpFlag help( parser, "help", "Display this help menu", { 'h', '?', "help" } ); args::Positional file( parser, "file", "The file or folder path" ); args::ValueFlag filePos( parser, "file", "The file or folder path", { 'f', "file", "folder" } ); @@ -3138,11 +3138,13 @@ EE_MAIN_FUNC int main( int argc, char* argv[] ) { { 'j', "jobs" }, 0 ); args::Flag health( parser, "health", "Checks for potential errors in editor setup.", { "health" }, "" ); - args::ValueFlag healthLang( parser, "health-lang", - "Checks for potential errors in editor setup.", - { "health-lang" }, "" ); + args::ValueFlag healthLang( + parser, "health-lang", + "Checks for potential errors in editor setup for a specific language.", { "health-lang" }, + "" ); args::MapFlag healthFormat( - parser, "health-format", "Checks for potential errors in editor setup.", + parser, "health-format", + "Sets the health format report (accepted values: terminal, markdown, ascii, asciidoc)", { "health-format" }, FeaturesHealth::getMapFlag(), FeaturesHealth::getDefaultOutputFormat() ); diff --git a/src/tools/ecode/plugins/formatter/formatterplugin.cpp b/src/tools/ecode/plugins/formatter/formatterplugin.cpp index b29f1961d..5eecb3f1a 100644 --- a/src/tools/ecode/plugins/formatter/formatterplugin.cpp +++ b/src/tools/ecode/plugins/formatter/formatterplugin.cpp @@ -79,7 +79,7 @@ void FormatterPlugin::onRegister( UICodeEditor* editor ) { listeners.push_back( editor->addEventListener( Event::OnDocumentLoaded, [&, editor]( const Event* ) { - tryRequestCapabilities( editor ); + tryRequestCapabilities( editor->getDocumentRef() ); } ) ); listeners.push_back( editor->addEventListener( Event::OnDocumentSave, [&]( @@ -271,7 +271,7 @@ std::string FormatterPlugin::getFileConfigPath() { bool FormatterPlugin::onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* menu, const Vector2i&, const Uint32& ) { - tryRequestCapabilities( editor ); + tryRequestCapabilities( editor->getDocumentRef() ); if ( supportsFormatter( editor->getDocumentRef() ).command.empty() && !supportsLSPFormatter( editor->getDocumentRef() ) ) return false; @@ -531,8 +531,8 @@ void FormatterPlugin::registerNativeFormatters() { }; } -bool FormatterPlugin::tryRequestCapabilities( UICodeEditor* editor ) { - const auto& language = editor->getDocumentRef()->getSyntaxDefinition().getLSPName(); +bool FormatterPlugin::tryRequestCapabilities( const std::shared_ptr& doc ) { + const auto& language = doc->getSyntaxDefinition().getLSPName(); auto it = mCapabilities.find( language ); if ( it != mCapabilities.end() ) return true; diff --git a/src/tools/ecode/plugins/formatter/formatterplugin.hpp b/src/tools/ecode/plugins/formatter/formatterplugin.hpp index 3f6097213..f6d71e928 100644 --- a/src/tools/ecode/plugins/formatter/formatterplugin.hpp +++ b/src/tools/ecode/plugins/formatter/formatterplugin.hpp @@ -115,7 +115,7 @@ class FormatterPlugin : public UICodeEditorPlugin { void registerNativeFormatters(); - bool tryRequestCapabilities( UICodeEditor* editor ); + bool tryRequestCapabilities( const std::shared_ptr& doc ); PluginRequestHandle processResponse( const PluginMessage& msg ); }; diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp index 6b85286d6..4e409dc45 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.cpp @@ -16,6 +16,97 @@ using json = nlohmann::json; namespace ecode { +class LSPLocationModel : public Model { + public: + enum CustomInfo { URI, TextRange }; + + static std::shared_ptr create( const std::string& workspaceFolder, + const std::vector& data ) { + return std::make_shared( workspaceFolder, data ); + } + + LSPLocationModel( const std::string& workspaceFolder, const std::vector& 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 {}; + } + + void update() override { onModelUpdate(); } + + protected: + struct Location { + LSPLocation loc; + std::string display; + }; + std::vector mLocs; + + void createLocs( std::string workspaceFolder, const std::vector& locs ) { + FileSystem::dirAddSlashAtEnd( workspaceFolder ); + + for ( const auto& loc : locs ) { + std::string display = loc.uri.getPath(); + 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 create( UISceneNode* sceneNode, + const std::vector& data ) { + return std::make_shared( sceneNode, data ); + } + + explicit LSPCodeActionModel( UISceneNode* sceneNode, const std::vector& 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(); } + + void update() override { onModelUpdate(); } + + const LSPCodeAction& getCodeAction( size_t row ) const { return mCodeActions[row]; } + + protected: + UISceneNode* mUISceneNode; + std::vector mCodeActions; +}; + UICodeEditorPlugin* LSPClientPlugin::New( PluginManager* pluginManager ) { return eeNew( LSPClientPlugin, ( pluginManager, false ) ); } @@ -116,7 +207,7 @@ PluginRequestHandle LSPClientPlugin::processCodeCompletionRequest( const PluginM PluginMessageFormat::CodeCompletion, &completionList, id ); } ); - return std::move( ret ); + return ret; } PluginRequestHandle LSPClientPlugin::processSignatureHelpRequest( const PluginMessage& msg ) { @@ -133,7 +224,7 @@ PluginRequestHandle LSPClientPlugin::processSignatureHelpRequest( const PluginMe PluginMessageFormat::SignatureHelp, &data, id ); } ); - return std::move( ret ); + return ret; } PluginRequestHandle LSPClientPlugin::processDocumentFormatting( const PluginMessage& msg ) { @@ -150,24 +241,36 @@ PluginRequestHandle LSPClientPlugin::processDocumentFormatting( const PluginMess auto ret = server.server->documentFormatting( server.uri, msg.asJSON()["options"], [&, server]( const PluginIDType&, const std::vector& edits ) { - processDocumentFormattingResponse( server.uri, edits ); + mManager->getSplitter()->getUISceneNode()->runOnMainThread( + [this, server, edits] { processDocumentFormattingResponse( server.uri, edits ); } ); } ); - return std::move( ret ); + return ret; } -void LSPClientPlugin::processDocumentFormattingResponse( const URI& uri, - const std::vector& edits ) { +bool LSPClientPlugin::processDocumentFormattingResponse( const URI& uri, + std::vector edits ) { auto doc = mManager->getSplitter()->findDocFromPath( uri.getPath() ); - if ( !doc ) - return; + if ( !doc ) { + auto pair = mManager->getSplitter()->loadFileFromPathInNewTab( uri.getPath() ); + if ( pair.first == nullptr || pair.second == nullptr || !pair.second->getDocumentRef() ) + return false; + doc = pair.second->getDocumentRef(); + } for ( const auto& edit : edits ) - if ( !edit.range.isValid() || !doc->isValidRange( edit.range ) ) - return; + if ( !edit.range.isValid() ) + return false; TextRanges ranges = doc->getSelections(); + 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() ) { @@ -175,94 +278,56 @@ void LSPClientPlugin::processDocumentFormattingResponse( const URI& uri, } else { if ( edit.range.hasSelection() ) doc->deleteTo( 0, 0 ); - doc->setSelection( 0, - doc->insert( 0, doc->getSelectionIndex( 0 ).start(), edit.text ) ); + doc->insert( 0, doc->getSelectionIndex( 0 ).start(), edit.text ); } } doc->setSelection( ranges ); + + doc->setRunningTransaction( false ); + + return true; } bool LSPClientPlugin::editorExists( UICodeEditor* editor ) { return mManager->getSplitter()->editorExists( editor ); } -class LSPLocationModel : public Model { - public: - enum CustomInfo { URI, TextRange }; - - static std::shared_ptr create( const std::string& workspaceFolder, - const std::vector& data ) { - return std::make_shared( workspaceFolder, data ); - } - - LSPLocationModel( const std::string& workspaceFolder, const std::vector& 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 ( 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 {}; - } - - void update() override { onModelUpdate(); } - - protected: - struct Location { - LSPLocation loc; - std::string display; - }; - std::vector mLocs; - - void createLocs( std::string workspaceFolder, const std::vector locs ) { - FileSystem::dirAddSlashAtEnd( workspaceFolder ); - - for ( const auto& loc : locs ) { - std::string display = loc.uri.getPath(); - FileSystem::filePathRemoveBasePath( workspaceFolder, display ); - display += " - L" + String::toString( loc.range.start().line() ); - mLocs.push_back( { loc, display } ); - } - } -}; - -void LSPClientPlugin::createLocationsView( UICodeEditor* editor, - const std::vector& res ) { +void LSPClientPlugin::createListView( UICodeEditor* editor, const std::shared_ptr& model, + const ModelEventCallback& onModelEventCb, + const std::function onCreateCb ) { UICodeEditorSplitter* splitter = getManager()->getSplitter(); if ( nullptr == splitter || !editorExists( editor ) ) return; - editor->runOnMainThread( [&, editor, splitter, res] { + editor->runOnMainThread( [model, editor, splitter, onModelEventCb, onCreateCb] { + auto lvs = editor->findAllByClass( "editor_listview" ); + for ( auto* ilv : lvs ) + ilv->close(); + UIListView* lv = UIListView::New(); lv->setParent( editor ); - auto pos = editor->getRelativeScreenPosition( editor->getDocumentRef()->getSelection().start() ); - lv->setPixelsPosition( { pos.x, pos.y + editor->getLineHeight() } ); - auto model = LSPLocationModel::create( mManager->getWorkspaceFolder(), res ); + lv->addClass( "editor_listview" ); + auto pos = + editor->getRelativeScreenPosition( editor->getDocumentRef()->getSelection().start() ); lv->setModel( model ); lv->setSelection( model->index( 0, 0 ) ); Float colWidth = lv->getMaxColumnContentWidth( 0 ) + PixelDensity::dpToPx( 4 ); lv->setColumnWidth( 0, colWidth ); - lv->setPixelsSize( { colWidth, lv->getContentSize().y } ); + lv->setPixelsSize( + { colWidth, std::min( lv->getContentSize().y, lv->getRowHeight() * 8 ) } ); + lv->setPixelsPosition( { pos.x, pos.y + editor->getLineHeight() } ); + if ( !lv->getParent()->getLocalBounds().contains( + lv->getLocalBounds().setPosition( lv->getPixelsPosition() ) ) ) { + lv->setPixelsPosition( { pos.x, pos.y - lv->getPixelsSize().getHeight() } ); + } lv->setVisible( true ); + if ( onCreateCb ) + onCreateCb( lv ); lv->setFocus(); Uint32 focusCb = lv->getUISceneNode()->getUIEventDispatcher()->addFocusEventCallback( - [lv, splitter, editor]( const auto&, Node* focus, Node* ) { - if ( !lv->inParentTreeOf( focus ) && !lv->isClosing() ) { + [lv]( const auto&, Node* focus, Node* ) { + if ( !lv->inParentTreeOf( focus ) && !lv->isClosing() ) lv->close(); - if ( splitter->editorExists( editor ) ) - editor->setFocus(); - } } ); Uint32 cursorCb = editor->on( Event::OnCursorPosChange, [lv, editor, splitter]( const Event* ) { @@ -272,28 +337,16 @@ void LSPClientPlugin::createLocationsView( UICodeEditor* editor, editor->setFocus(); } } ); - lv->on( Event::KeyDown, [splitter, editor, lv]( const Event* event ) { - if ( event->asKeyEvent()->getKeyCode() == EE::Window::KEY_ESCAPE && !lv->isClosing() ) { + lv->on( Event::KeyDown, [lv, splitter, editor]( const Event* event ) { + if ( event->asKeyEvent()->getKeyCode() == EE::Window::KEY_ESCAPE && !lv->isClosing() ) lv->close(); - if ( splitter->editorExists( editor ) ) - editor->setFocus(); - } + if ( splitter->editorExists( editor ) ) + editor->setFocus(); } ); - lv->on( Event::OnModelEvent, [&]( const Event* event ) { + lv->on( Event::OnModelEvent, [&, onModelEventCb]( const Event* event ) { const ModelEvent* modelEvent = static_cast( event ); - 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 ); + if ( onModelEventCb ) + onModelEventCb( modelEvent ); } ); lv->on( Event::OnClose, [lv, editor, cursorCb, focusCb]( const Event* ) { lv->getUISceneNode()->getUIEventDispatcher()->removeFocusEventCallback( focusCb ); @@ -302,6 +355,63 @@ void LSPClientPlugin::createLocationsView( UICodeEditor* editor, } ); } +void LSPClientPlugin::createLocationsView( UICodeEditor* editor, + const std::vector& res ) { + auto model = LSPLocationModel::create( mManager->getWorkspaceFolder(), res ); + createListView( editor, model, [&]( 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& cas ) { + auto model = LSPCodeActionModel::create( mManager->getSplitter()->getUISceneNode(), cas ); + createListView( + editor, model, + [this, editor]( const ModelEvent* modelEvent ) { + if ( modelEvent->getModelEventType() != ModelEventType::Open ) + return; + auto r = modelEvent->getModelIndex().row(); + const auto cam = static_cast( modelEvent->getModel() ); + if ( cam->hasCodeActions() && editorExists( editor ) ) { + const auto& ca = cam->getCodeAction( r ); + auto server = mClientManager.getOneLSPClientServer( editor->getDocumentRef() ); + if ( server && server->getCapabilities().executeCommandProvider ) { + mClientManager.executeCommand( editor->getDocumentRef(), ca.command ); + } else { + mClientManager.applyWorkspaceEdit( ca.edit, []( const auto& ) {} ); + } + editor->setFocus(); + } + modelEvent->getNode()->close(); + }, + [this, cas, editor]( UIListView* lv ) { + if ( cas.empty() ) { + 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 {}; @@ -383,11 +493,11 @@ void LSPClientPlugin::load( PluginManager* pluginManager ) { std::vector lsps; - for ( const auto& path : paths ) { + for ( const auto& ipath : paths ) { try { - loadLSPConfig( lsps, path, mConfigPath == path ); + loadLSPConfig( lsps, ipath, mConfigPath == ipath ); } catch ( const json::exception& e ) { - Log::error( "Parsing LSP \"%s\" failed:\n%s", path.c_str(), e.what() ); + Log::error( "Parsing LSP \"%s\" failed:\n%s", ipath.c_str(), e.what() ); } } @@ -434,14 +544,15 @@ void LSPClientPlugin::loadLSPConfig( std::vector& lsps, const std if ( mKeyBindings.empty() ) { mKeyBindings["lsp-go-to-definition"] = "f2"; mKeyBindings["lsp-symbol-info"] = "f1"; + mKeyBindings["lsp-symbol-code-action"] = "alt+return"; } 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" }; + 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" }; for ( const auto& key : list ) { if ( kb.contains( key ) ) { if ( !kb[key].empty() ) @@ -597,6 +708,14 @@ void LSPClientPlugin::getAndGoToLocation( UICodeEditor* editor, const std::strin } ); } +void LSPClientPlugin::codeAction( UICodeEditor* editor ) { + mClientManager.codeAction( + editor->getDocumentRef(), + [&, editor]( const LSPClientServer::IdType&, const std::vector& res ) { + createCodeActionsView( editor, res ); + } ); +} + void LSPClientPlugin::onRegister( UICodeEditor* editor ) { Lock l( mDocMutex ); mDocs.insert( editor->getDocumentRef().get() ); @@ -634,6 +753,8 @@ void LSPClientPlugin::onRegister( UICodeEditor* editor ) { mClientManager.getSymbolReferences( editor->getDocumentRef() ); } ); + doc.setCommand( "lsp-symbol-code-action", [&, editor] { codeAction( editor ); } ); + doc.setCommand( "lsp-memory-usage", [&, editor] { mClientManager.memoryUsage( editor->getDocumentRef() ); } ); } @@ -697,14 +818,14 @@ void LSPClientPlugin::onUnregister( UICodeEditor* editor ) { return; Lock l( mDocMutex ); TextDocument* doc = mEditorDocs[editor]; - auto& cbs = mEditors[editor]; + const auto& cbs = mEditors[editor]; for ( auto listener : cbs ) editor->removeEventListener( listener ); mEditors.erase( editor ); mEditorsTags.erase( editor ); mEditorDocs.erase( editor ); - for ( auto& editor : mEditorDocs ) - if ( editor.second == doc ) + for ( const auto& ieditor : mEditorDocs ) + if ( ieditor.second == doc ) return; mDocs.erase( doc ); } @@ -745,6 +866,9 @@ bool LSPClientPlugin::onCreateContextMenu( UICodeEditor* editor, UIPopUpMenu* me if ( cap.referencesProvider ) addFn( "lsp-symbol-references", "Find References to Symbol Under Cursor" ); + if ( cap.codeActionProvider ) + addFn( "lsp-symbol-code-action", "Code Action" ); + if ( server->getDefinition().language == "cpp" || server->getDefinition().language == "c" ) addFn( "lsp-switch-header-source", "Switch Header/Source" ); diff --git a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp index 9ca06d9d4..7367b1ff5 100644 --- a/src/tools/ecode/plugins/lsp/lspclientplugin.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientplugin.hpp @@ -8,8 +8,10 @@ #include #include #include +#include #include #include +#include #include using namespace EE; using namespace EE::System; @@ -69,6 +71,8 @@ class LSPClientPlugin : public UICodeEditorPlugin { std::string getFileConfigPath(); + bool processDocumentFormattingResponse( const URI& uri, std::vector edits ); + protected: PluginManager* mManager{ nullptr }; std::shared_ptr mThreadPool; @@ -125,13 +129,21 @@ class LSPClientPlugin : public UICodeEditorPlugin { void switchSourceHeader( UICodeEditor* editor ); - void processDocumentFormattingResponse( const URI& uri, const std::vector& edits ); - bool editorExists( UICodeEditor* editor ); void createLocationsView( UICodeEditor* editor, const std::vector& locs ); void getAndGoToLocation( UICodeEditor* editor, const std::string& search ); + + void codeAction( UICodeEditor* editor ); + + void createCodeActionsView( UICodeEditor* editor, const std::vector& cas ); + + typedef std::function ModelEventCallback; + + void createListView( UICodeEditor* editor, const std::shared_ptr& model, + const ModelEventCallback& onModelEventCb, + const std::function onCreateCb = {} ); }; } // namespace ecode diff --git a/src/tools/ecode/plugins/lsp/lspclientserver.cpp b/src/tools/ecode/plugins/lsp/lspclientserver.cpp index 285ba379d..2980966af 100644 --- a/src/tools/ecode/plugins/lsp/lspclientserver.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientserver.cpp @@ -327,15 +327,13 @@ static void fromJson( LSPServerCapabilities& caps, const json& json ) { if ( json.contains( "documentOnTypeFormattingProvider" ) ) fromJson( caps.documentOnTypeFormattingProvider, json["documentOnTypeFormattingProvider"] ); caps.renameProvider = toBoolOrObject( json, "renameProvider" ); - if ( json.contains( "codeActionProvider" ) && - json["codeActionProvider"].contains( "resolveProvider" ) ) { - auto& codeActionProvider = json["codeActionProvider"]; - caps.codeActionProvider = codeActionProvider["resolveProvider"].get(); - } + caps.codeActionProvider = json.contains( "codeActionProvider" ); + caps.executeCommandProvider = json.contains( "executeCommandProvider" ); // fromJson( caps.semanticTokenProvider, json["semanticTokensProvider"] ); if ( json.contains( "workspace" ) ) { auto& workspace = json["workspace"]; - fromJson( caps.workspaceFolders, workspace["workspaceFolders"] ); + if ( workspace.contains( "workspaceFolders" ) ) + fromJson( caps.workspaceFolders, workspace["workspaceFolders"] ); } caps.selectionRangeProvider = toBoolOrObject( json, "selectionRangeProvider" ); caps.ready = true; @@ -531,8 +529,9 @@ static std::vector parseCodeAction( const json& result ) { 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 edit = action.contains( MEMBER_EDIT ) + ? parseWorkSpaceEdit( action.at( MEMBER_EDIT ) ) + : LSPWorkspaceEdit{}; auto diagnostics = action.contains( MEMBER_DIAGNOSTICS ) ? parseDiagnostics( action.at( MEMBER_DIAGNOSTICS ) ) : std::vector{}; @@ -611,10 +610,9 @@ static json codeActionParams( const URI& document, const TextRange& range, auto params = textDocumentParams( document ); params[MEMBER_RANGE] = toJson( range ); json context; - json diags; - for ( const auto& diagnostic : diagnostics ) { + json diags = json::array(); + for ( const auto& diagnostic : diagnostics ) diags.push_back( toJson( diagnostic ) ); - } context[MEMBER_DIAGNOSTICS] = diags; if ( !kinds.empty() ) context["only"] = json( kinds ); @@ -855,6 +853,22 @@ static LSPShowDocumentParams parseShowDocument( const json& json ) { return params; } +static LSPApplyWorkspaceEditParams parseApplyWorkspaceEditParams( const json& result ) { + LSPApplyWorkspaceEditParams ret; + ret.label = result.value( MEMBER_LABEL, "" ); + if ( result.contains( MEMBER_EDIT ) ) + ret.edit = parseWorkSpaceEdit( result.at( MEMBER_EDIT ) ); + return ret; +} + +static json applyWorkspaceEditResponse( const PluginIDType& msgid, + const LSPApplyWorkspaceEditResponse& response ) { + json j = newID( msgid ); + j[MEMBER_RESULT] = + json{ { "applied", response.applied }, { "failureReason", response.failureReason } }; + return j; +} + void LSPClientServer::initialize() { json codeAction{ { "codeActionLiteralSupport", @@ -899,16 +913,18 @@ void LSPClientServer::initialize() { { "capabilities", capabilities }, { "initializationOptions", mLSP.initializationOptions } }; - URI rootPath( mRootPath ); + URI rootPath( String::startsWith( mRootPath, "file://" ) ? mRootPath : "file://" + mRootPath ); if ( rootPath.empty() ) { - if ( !mManager->getLSPWorkspaceFolder().uri.empty() ) + if ( !mManager->getLSPWorkspaceFolder().uri.empty() ) { rootPath = mManager->getLSPWorkspaceFolder().uri; - else + if ( rootPath.getScheme().empty() ) + rootPath = URI( "file://" + mManager->getLSPWorkspaceFolder().uri.toString() ); + } else { rootPath = URI( "file://" + FileSystem::getCurrentWorkingDirectory() ); + } } - auto authAndPath = rootPath.getAuthorityAndPath(); - auto rootUri = rootPath.toString(); - params["rootPath"] = authAndPath; + std::string rootUri = rootPath.toString(); + params["rootPath"] = rootPath.getPath(); params["rootUri"] = rootUri; params["workspaceFolders"] = toJson( { LSPWorkspaceFolder{ rootUri, FileSystem::fileNameFromPath( rootUri ) } } ); @@ -931,6 +947,11 @@ void LSPClientServer::initialize() { mReady = true; write( newRequest( "initialized" ) ); sendQueuedMessages(); + + // Broadcast the language capabilities to all the interested plugins + mManager->getPluginManager()->sendBroadcast( + mManager->getPlugin(), PluginMessageType::LanguageServerCapabilities, + PluginMessageFormat::LanguageServerCapabilities, &mCapabilities ); }, [&]( const IdType&, const json& ) {} ); } @@ -1306,7 +1327,17 @@ void LSPClientServer::processRequest( const json& msg ) { msg.dump().c_str() ); auto method = msg[MEMBER_METHOD].get(); auto msgid = getID( msg ); - if ( method == "window/workDoneProgress/create" || method == "client/registerCapability" ) { + if ( method == "workspace/applyEdit" ) { + auto workspaceEdit = parseApplyWorkspaceEditParams( msg[MEMBER_PARAMS] ); + mManager->applyWorkspaceEdit( workspaceEdit.edit, + [&, msgid]( const LSPApplyWorkspaceEditResponse& res ) { + getThreadPool()->run( [this, msgid, res] { + write( applyWorkspaceEditResponse( msgid, res ) ); + } ); + } ); + return; + } else if ( method == "window/workDoneProgress/create" || + method == "client/registerCapability" ) { write( newEmptyResult( msgid ) ); return; } else if ( method == "window/showMessageRequest" ) { @@ -1680,7 +1711,8 @@ LSPClientServer::LSPRequestHandle LSPClientServer::memoryUsage() { LSPClientServer::LSPRequestHandle LSPClientServer::executeCommand( const std::string& cmd, const json& params ) { - return send( executeCommandParams( cmd, params ), []( const auto&, const auto& ) {} ); + return send( newRequest( "workspace/executeCommand", executeCommandParams( cmd, params ) ), + []( const auto&, const auto& ) {} ); } LSPClientServer::LSPRequestHandle diff --git a/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp b/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp index 7060f9e2b..2ffd60ba4 100644 --- a/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp +++ b/src/tools/ecode/plugins/lsp/lspclientservermanager.cpp @@ -148,6 +148,41 @@ void LSPClientServerManager::goToLocation( const LSPLocation& loc ) { } ); } +void LSPClientServerManager::executeCommand( const std::shared_ptr& doc, + const LSPCommand& cmd ) { + if ( cmd.command.empty() ) + return; + auto* server = getOneLSPClientServer( doc ); + if ( server ) + server->executeCommand( cmd.command, cmd.arguments ); +} + +void LSPClientServerManager::applyWorkspaceEdit( + const LSPWorkspaceEdit& edit, + const std::function& resCb ) { + mPluginManager->getSplitter()->getUISceneNode()->runOnMainThread( [this, edit, resCb] { + bool allDone = true; + + for ( const auto& ed : edit.changes ) { + if ( !mPlugin->processDocumentFormattingResponse( ed.first, ed.second ) ) { + allDone = false; + } + } + + for ( const auto& edc : edit.documentChanges ) { + if ( !mPlugin->processDocumentFormattingResponse( edc.textDocument.uri, edc.edits ) ) { + allDone = false; + } + } + + if ( resCb ) { + LSPApplyWorkspaceEditResponse res; + res.applied = allDone; + resCb( res ); + } + } ); +} + void LSPClientServerManager::run( const std::shared_ptr& doc ) { mThreadPool->run( [&, doc]() { tryRunServer( doc ); }, []() {} ); } @@ -201,7 +236,7 @@ void LSPClientServerManager::updateDirty() { mLSPsToClose.erase( invalided ); } if ( !removed.empty() ) { - for ( auto& remove : removed ) + for ( const auto& remove : removed ) closeLSPServer( remove ); } } @@ -288,6 +323,21 @@ void LSPClientServerManager::getSymbolReferences( std::shared_ptr } ); } +void LSPClientServerManager::codeAction( std::shared_ptr doc, + const LSPClientServer::CodeActionHandler& h ) { + auto* server = getOneLSPClientServer( doc ); + if ( !server ) + return; + + auto range = doc->getSelection(); + if ( !doc->hasSelection() ) { + range = { doc->startOfLine( range.start() ), + doc->positionOffset( doc->endOfLine( range.end() ), 1 ) }; + } + + server->documentCodeAction( doc->getURI(), range, {}, {}, h ); +} + void LSPClientServerManager::memoryUsage( std::shared_ptr doc ) { auto* server = getOneLSPClientServer( doc ); if ( !server ) @@ -380,7 +430,6 @@ LSPClientServer* LSPClientServerManager::getOneLSPClientServer( const URI& uri ) } LSPClientServer* LSPClientServerManager::getOneLSPClientServer( const std::string& language ) { - std::vector servers; Lock l( mClientsMutex ); for ( auto& server : mClients ) { if ( server.second->getDefinition().language == language ) diff --git a/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp b/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp index e6b1a17d8..1030eb2d9 100644 --- a/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp +++ b/src/tools/ecode/plugins/lsp/lspclientservermanager.hpp @@ -66,6 +66,9 @@ class LSPClientServerManager { void getSymbolReferences( std::shared_ptr doc ); + void codeAction( std::shared_ptr doc, + const LSPClientServer::CodeActionHandler& h ); + void memoryUsage( std::shared_ptr doc ); const std::vector& getLSPs() const; @@ -78,6 +81,12 @@ class LSPClientServerManager { void goToLocation( const LSPLocation& loc ); + void executeCommand( const std::shared_ptr& doc, const LSPCommand& cmd ); + + void applyWorkspaceEdit( + const LSPWorkspaceEdit& edit, + const std::function& resCb ); + protected: friend class LSPClientServer; PluginManager* mPluginManager{ nullptr }; diff --git a/src/tools/ecode/plugins/lsp/lspprotocol.hpp b/src/tools/ecode/plugins/lsp/lspprotocol.hpp index 58c2c61ff..c9a4e7d16 100644 --- a/src/tools/ecode/plugins/lsp/lspprotocol.hpp +++ b/src/tools/ecode/plugins/lsp/lspprotocol.hpp @@ -111,6 +111,7 @@ struct LSPServerCapabilities { // (other parts not useful/considered at present) LSPWorkspaceFoldersServerCapabilities workspaceFolders; bool selectionRangeProvider = false; + bool executeCommandProvider = false; }; enum class LSPMessageType { Error = 1, Warning = 2, Info = 3, Log = 4 }; @@ -421,6 +422,16 @@ struct LSPShowDocumentParams { TextRange selection; }; +struct LSPApplyWorkspaceEditParams { + std::string label; + LSPWorkspaceEdit edit; +}; + +struct LSPApplyWorkspaceEditResponse { + bool applied; + std::string failureReason; +}; + } // namespace ecode #endif // ECODE_LSPCLIENTPROTOCOL_HPP