diff --git a/bin/assets/formatter/formatter.json b/bin/assets/formatter/formatter.json new file mode 100644 index 000000000..eb0837beb --- /dev/null +++ b/bin/assets/formatter/formatter.json @@ -0,0 +1,10 @@ +[ + { + "file_patterns": ["%.js$", "%.ts$"], + "command": "prettier $FILENAME" + }, + { + "file_patterns": ["%.cpp$", "%.h$", "%.hpp$"], + "command": "clang-format --style=file $FILENAME" + } +] diff --git a/include/eepp/ui/doc/textdocument.hpp b/include/eepp/ui/doc/textdocument.hpp index 456dcc325..d41ca88a3 100644 --- a/include/eepp/ui/doc/textdocument.hpp +++ b/include/eepp/ui/doc/textdocument.hpp @@ -251,6 +251,8 @@ class EE_API TextDocument { void setCommand( const std::string& command, DocumentCommand func ); + bool hasCommand( const std::string& command ); + TextRange find( String text, TextPosition from = { 0, 0 }, const bool& caseSensitive = true, const bool& wholeWord = false, const FindReplaceType& type = FindReplaceType::Normal, diff --git a/projects/linux/ee.files b/projects/linux/ee.files index b2cbf3da8..3382ac845 100644 --- a/projects/linux/ee.files +++ b/projects/linux/ee.files @@ -1103,6 +1103,8 @@ ../../src/tools/codeeditor/filelocator.hpp ../../src/tools/codeeditor/filesystemlistener.cpp ../../src/tools/codeeditor/filesystemlistener.hpp +../../src/tools/codeeditor/formattermodule.cpp +../../src/tools/codeeditor/formattermodule.hpp ../../src/tools/codeeditor/globalsearchcontroller.cpp ../../src/tools/codeeditor/globalsearchcontroller.hpp ../../src/tools/codeeditor/ignorematcher.cpp diff --git a/src/eepp/ui/doc/textdocument.cpp b/src/eepp/ui/doc/textdocument.cpp index 2140219d0..0652693d1 100644 --- a/src/eepp/ui/doc/textdocument.cpp +++ b/src/eepp/ui/doc/textdocument.cpp @@ -1246,6 +1246,10 @@ void TextDocument::setCommand( const std::string& command, TextDocument::Documen mCommands[command] = func; } +bool TextDocument::hasCommand( const std::string& command ) { + return mCommands.find( command ) != mCommands.end(); +} + static std::pair findType( const String& str, const String& findStr, const TextDocument::FindReplaceType& type ) { switch ( type ) { diff --git a/src/tools/codeeditor/appconfig.cpp b/src/tools/codeeditor/appconfig.cpp index 325819788..99f0f3c5a 100644 --- a/src/tools/codeeditor/appconfig.cpp +++ b/src/tools/codeeditor/appconfig.cpp @@ -70,6 +70,7 @@ void AppConfig::load( std::string& confPath, std::string& keybindingsPath, editor.colorPreview = ini.getValueB( "editor", "color_preview", true ); editor.autoComplete = ini.getValueB( "editor", "auto_complete", true ); editor.linter = ini.getValueB( "editor", "linter", true ); + editor.formatter = ini.getValueB( "editor", "formatter", true ); editor.showDocInfo = ini.getValueB( "editor", "show_doc_info", true ); editor.hideTabBarOnSingleTab = ini.getValueB( "editor", "hide_tab_bar_on_single_tab", true ); editor.singleClickTreeNavigation = ini.getValueB( "editor", "single_click_tree_navigation", false ); @@ -116,6 +117,7 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "editor", "color_preview", editor.colorPreview ); ini.setValueB( "editor", "auto_complete", editor.autoComplete ); ini.setValueB( "editor", "linter", editor.linter ); + ini.setValueB( "editor", "formatter", editor.formatter ); ini.setValueB( "editor", "show_doc_info", editor.showDocInfo ); ini.setValueB( "editor", "hide_tab_bar_on_single_tab", editor.hideTabBarOnSingleTab ); ini.setValueB( "editor", "single_click_tree_navigation", editor.singleClickTreeNavigation ); diff --git a/src/tools/codeeditor/appconfig.hpp b/src/tools/codeeditor/appconfig.hpp index 0dd058c3c..6c447188d 100644 --- a/src/tools/codeeditor/appconfig.hpp +++ b/src/tools/codeeditor/appconfig.hpp @@ -51,6 +51,7 @@ struct CodeEditorConfig { bool autoComplete{ true }; bool showDocInfo{ true }; bool linter{ true }; + bool formatter{ true }; bool hideTabBarOnSingleTab{ true }; bool singleClickTreeNavigation{ false }; std::string autoCloseBrackets{ "" }; diff --git a/src/tools/codeeditor/codeeditor.cpp b/src/tools/codeeditor/codeeditor.cpp index 0809c6c64..22dd65c97 100644 --- a/src/tools/codeeditor/codeeditor.cpp +++ b/src/tools/codeeditor/codeeditor.cpp @@ -1,5 +1,6 @@ #include "codeeditor.hpp" #include "autocompletemodule.hpp" +#include "formattermodule.hpp" #include "lintermodule.hpp" #include #include @@ -372,6 +373,7 @@ App::~App() { eeSAFE_DELETE( mEditorSplitter ); eeSAFE_DELETE( mAutoCompleteModule ); eeSAFE_DELETE( mLinterModule ); + eeSAFE_DELETE( mFormatterModule ); eeSAFE_DELETE( mConsole ); if ( mFileWatcher ) delete mFileWatcher; @@ -609,6 +611,9 @@ UIMenu* App::createViewMenu() { ->setActive( mConfig.editor.linter ) ->setTooltipText( "Use static code analysis tool used to flag programming errors, bugs,\n" "stylistic errors, and suspicious constructs." ); + mViewMenu->addCheckBox( "Enable Code Formatter" ) + ->setActive( mConfig.editor.formatter ) + ->setTooltipText( "Enables the code formatter/prettifier module." ); mViewMenu->addCheckBox( "Hide tabbar on single tab" ) ->setActive( mConfig.editor.hideTabBarOnSingleTab ) ->setTooltipText( "Hides the tabbar if there's only one element in the tab widget." ); @@ -661,6 +666,8 @@ UIMenu* App::createViewMenu() { setAutoComplete( item->asType()->isActive() ); } else if ( item->getText() == "Enable Linter" ) { setLinter( item->asType()->isActive() ); + } else if ( item->getText() == "Enable Code Formatter" ) { + setFormatter( item->asType()->isActive() ); } else if ( item->getText() == "Enable Color Preview" ) { mConfig.editor.colorPreview = item->asType()->isActive(); mEditorSplitter->forEachEditor( [&]( UICodeEditor* editor ) { @@ -1100,7 +1107,8 @@ std::map App::getLocalKeybindings() { { { KEY_L, KEYMOD_CTRL }, "go-to-line" }, { { KEY_M, KEYMOD_CTRL }, "menu-toggle" }, { { KEY_S, KEYMOD_CTRL | KEYMOD_SHIFT }, "save-all" }, - { { KEY_F9, KEYMOD_LALT }, "switch-side-panel" } }; + { { KEY_F9, KEYMOD_LALT }, "switch-side-panel" }, + { { KEY_F, KEYMOD_LALT }, "format-doc" } }; } std::vector App::getUnlockedCommands() { @@ -1403,8 +1411,14 @@ void App::onCodeEditorCreated( UICodeEditor* editor, TextDocument& doc ) { if ( config.linter && !mLinterModule ) setLinter( config.linter ); + if ( config.formatter && !mFormatterModule ) + setFormatter( config.formatter ); + if ( config.linter && mLinterModule ) editor->registerModule( mLinterModule ); + + if ( config.formatter && mFormatterModule ) + editor->registerModule( mFormatterModule ); } bool App::setAutoComplete( bool enable ) { @@ -1436,6 +1450,22 @@ bool App::setLinter( bool enable ) { return false; } +bool App::setFormatter( bool enable ) { + mConfig.editor.formatter = enable; + if ( enable && !mFormatterModule ) { + std::string path( mResPath + "assets/formatter/formatter.json" ); + if ( FileSystem::fileExists( mConfigPath + "formatter.json" ) ) + path = mConfigPath + "formatter.json"; + mFormatterModule = eeNew( FormatterModule, ( path, mThreadPool ) ); + mEditorSplitter->forEachEditor( + [&]( UICodeEditor* editor ) { editor->registerModule( mFormatterModule ); } ); + return true; + } + if ( !enable && mFormatterModule ) + eeSAFE_DELETE( mFormatterModule ); + return false; +} + void App::loadCurrentDirectory() { if ( !mEditorSplitter->getCurEditor() ) return; diff --git a/src/tools/codeeditor/codeeditor.hpp b/src/tools/codeeditor/codeeditor.hpp index b66e24893..89b3abc2c 100644 --- a/src/tools/codeeditor/codeeditor.hpp +++ b/src/tools/codeeditor/codeeditor.hpp @@ -16,6 +16,7 @@ class AutoCompleteModule; class LinterModule; +class FormatterModule; class App : public UICodeEditorSplitter::Client { public: @@ -101,6 +102,7 @@ class App : public UICodeEditorSplitter::Client { std::string mResPath; AutoCompleteModule* mAutoCompleteModule{ nullptr }; LinterModule* mLinterModule{ nullptr }; + FormatterModule* mFormatterModule{ nullptr }; std::shared_ptr mThreadPool; std::unique_ptr mDirTree; UITreeView* mProjectTreeView{ nullptr }; @@ -208,6 +210,8 @@ class App : public UICodeEditorSplitter::Client { bool setLinter( bool enable ); + bool setFormatter( bool enable ); + void updateDocInfo( TextDocument& doc ); void setFocusEditorOnClose( UIMessageBox* msgBox ); diff --git a/src/tools/codeeditor/formattermodule.cpp b/src/tools/codeeditor/formattermodule.cpp new file mode 100644 index 000000000..f57e0e0c7 --- /dev/null +++ b/src/tools/codeeditor/formattermodule.cpp @@ -0,0 +1,150 @@ +#include "formattermodule.hpp" +#include "thirdparty/json.hpp" +#include "thirdparty/subprocess.h" +#include + +using json = nlohmann::json; + +#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN || defined( __EMSCRIPTEN_PTHREADS__ ) +#define FORMATTER_THREADED 1 +#else +#define FORMATTER_THREADED 0 +#endif + +FormatterModule::FormatterModule( const std::string& formattersPath, + std::shared_ptr pool ) : + mPool( pool ) { +#if FORMATTER_THREADED + mPool->run( [&, formattersPath] { load( formattersPath ); }, [] {} ); +#else + load( formattersPath ); +#endif +} + +FormatterModule::~FormatterModule() { + mClosing = true; + for ( auto editor : mEditors ) + editor->unregisterModule( this ); +} + +void FormatterModule::onRegister( UICodeEditor* editor ) { + mEditors.insert( editor ); + + auto& doc = editor->getDocument(); + + if ( doc.hasCommand( "format-doc" ) ) + return; + + doc.setCommand( "format-doc", [&, editor]() { formatDoc( editor ); } ); +} + +void FormatterModule::onUnregister( UICodeEditor* editor ) { + if ( mClosing ) + return; + mEditors.erase( editor ); +} + +void FormatterModule::load( const std::string& formatterPath ) { + if ( !FileSystem::fileExists( formatterPath ) ) + return; + try { + std::ifstream stream( formatterPath ); + json j; + stream >> j; + + for ( auto& obj : j ) { + Formatter formatter; + auto fp = obj["file_patterns"]; + + for ( auto& pattern : fp ) + formatter.files.push_back( pattern.get() ); + + formatter.command = obj["command"].get(); + + if ( obj.contains( "type" ) ) { + std::string typeStr( obj["type"].get() ); + String::toLowerInPlace( typeStr ); + String::trimInPlace( typeStr ); + formatter.type = + "inplace" == typeStr ? FormatterType::Inplace : FormatterType::Output; + } + + mFormatters.emplace_back( std::move( formatter ) ); + } + + mReady = true; + } catch ( json::exception& e ) { + mReady = false; + Log::error( "Parsing formatter failed:\n%s", e.what() ); + } +} + +void FormatterModule::formatDoc( UICodeEditor* editor ) { + if ( !mReady ) + return; + Clock clock; + std::shared_ptr doc = editor->getDocumentRef(); + auto formatter = supportsFormatter( doc ); + if ( formatter.command.empty() && doc->getFilePath().empty() ) + return; + std::string cmd( formatter.command ); + std::string path( doc->getFilePath() ); + String::replaceAll( cmd, "$FILENAME", path ); + std::vector cmdArr = String::split( cmd, ' ' ); + std::vector strings; + for ( size_t i = 0; i < cmdArr.size(); ++i ) + strings.push_back( cmdArr[i].c_str() ); + strings.push_back( NULL ); + struct subprocess_s subprocess; + int result = subprocess_create( strings.data(), + subprocess_option_inherit_environment | + subprocess_option_combined_stdout_stderr, + &subprocess ); + if ( 0 == result ) { + std::string buffer( 1024, '\0' ); + std::string data; + unsigned index = 0; + unsigned bytesRead = 0; + do { + bytesRead = subprocess_read_stdout( &subprocess, &buffer[0], buffer.size() ); + index += bytesRead; + data += buffer.substr( 0, bytesRead ); + } while ( bytesRead != 0 ); + + int ret; + subprocess_join( &subprocess, &ret ); + subprocess_destroy( &subprocess ); + + // Log::info( "Formatter result:\n%s", data.c_str() ); + + if ( formatter.type == FormatterType::Output ) { + editor->runOnMainThread( [&, data, editor]() { + std::shared_ptr doc = editor->getDocumentRef(); + TextPosition pos = doc->getSelection().start(); + doc->selectAll(); + doc->textInput( data ); + doc->setSelection( pos ); + } ); + } + + Log::info( "FormatterModule::formatDoc for %s took %.2fms", path.c_str(), + clock.getElapsedTime().asMilliseconds() ); + } +} + +FormatterModule::Formatter FormatterModule::supportsFormatter( std::shared_ptr doc ) { + std::string filePath( doc->getFilePath() ); + std::string extension( FileSystem::fileExtension( filePath ) ); + if ( extension.empty() ) + extension = FileSystem::fileNameFromPath( filePath ); + const auto& def = doc->getSyntaxDefinition(); + for ( auto& formatter : mFormatters ) { + for ( auto& ext : formatter.files ) { + auto& files = def.getFiles(); + if ( std::find( files.begin(), files.end(), ext ) != files.end() ) { + return formatter; + } + } + } + return {}; +} diff --git a/src/tools/codeeditor/formattermodule.hpp b/src/tools/codeeditor/formattermodule.hpp new file mode 100644 index 000000000..9683df0be --- /dev/null +++ b/src/tools/codeeditor/formattermodule.hpp @@ -0,0 +1,46 @@ +#ifndef FORMATTERMODULE_HPP +#define FORMATTERMODULE_HPP + +#include +#include +#include +#include +#include +using namespace EE; +using namespace EE::System; +using namespace EE::UI; + +class FormatterModule : public UICodeEditorModule { + public: + FormatterModule( const std::string& formatterPath, std::shared_ptr pool ); + + virtual ~FormatterModule(); + + void onRegister( UICodeEditor* ); + + void onUnregister( UICodeEditor* ); + + protected: + enum class FormatterType { Inplace, Output }; + + struct Formatter { + std::vector files; + std::string command; + FormatterType type{ FormatterType::Output }; + }; + + std::shared_ptr mPool; + std::vector mFormatters; + std::set mEditors; + bool mClosing{ false }; + + bool mReady{ false }; + + void load( const std::string& formatterPath ); + + void formatDoc( UICodeEditor* editor ); + + FormatterModule::Formatter supportsFormatter( std::shared_ptr doc ); +}; + +#endif // FORMATTERMODULE_HPP diff --git a/src/tools/codeeditor/lintermodule.cpp b/src/tools/codeeditor/lintermodule.cpp index 159f65534..046581816 100644 --- a/src/tools/codeeditor/lintermodule.cpp +++ b/src/tools/codeeditor/lintermodule.cpp @@ -176,6 +176,8 @@ static std::string randString( size_t len ) { } void LinterModule::lintDoc( std::shared_ptr doc ) { + if ( !mReady ) + return; auto linter = supportsLinter( doc ); if ( linter.command.empty() ) return; @@ -232,7 +234,7 @@ void LinterModule::runLinter( std::shared_ptr doc, const Linter& l // Log::info( "Linter result:\n%s", data.c_str() ); - std::map matches; + std::map> matches; for ( auto& warningPatterm : linter.warningPattern ) { LuaPattern pattern( warningPatterm ); @@ -265,7 +267,7 @@ void LinterModule::runLinter( std::shared_ptr doc, const Linter& l } linterMatch.pos = { line - 1, col > 0 ? col - 1 : 0 }; linterMatch.lineCache = doc->line( line - 1 ).getHash(); - matches.insert( { line - 1, std::move( linterMatch ) } ); + matches[line - 1].emplace_back( std::move( linterMatch ) ); } } } @@ -292,42 +294,46 @@ void LinterModule::drawAfterLineText( UICodeEditor* editor, const Int64& index, } mMatchesMutex.unlock(); - std::map& map = matchIt->second; + std::map>& map = matchIt->second; auto lineIt = map.find( index ); if ( lineIt == map.end() ) return; TextDocument* doc = matchIt->first; - LinterMatch& match = lineIt->second; - if ( match.lineCache != doc->line( index ).getHash() ) - return; - Text line( "", editor->getFont(), editor->getFontSize() ); - line.setTabWidth( editor->getTabWidth() ); - line.setStyleConfig( editor->getFontStyleConfig() ); - line.setColor( editor->getColorScheme() - .getEditorSyntaxStyle( match.type == LinterType::Warning || - match.type == LinterType::Notice - ? "warning" - : "error" ) - .color ); - const String& text = doc->line( index ).getText(); - size_t minCol = text.find_first_not_of( " \t\f\v\n\r", match.pos.column() ); - if ( minCol == String::InvalidPos ) - minCol = match.pos.column(); - minCol = std::max( (Int64)minCol, match.pos.column() ); - if ( minCol >= text.size() ) - minCol = match.pos.column(); - if ( minCol >= text.size() ) - minCol = text.size() - 1; + std::vector& matches = lineIt->second; + for ( auto& match : matches ) { + if ( match.lineCache != doc->line( index ).getHash() ) + return; + Text line( "", editor->getFont(), editor->getFontSize() ); + line.setTabWidth( editor->getTabWidth() ); + line.setStyleConfig( editor->getFontStyleConfig() ); + line.setColor( editor->getColorScheme() + .getEditorSyntaxStyle( match.type == LinterType::Warning || + match.type == LinterType::Notice + ? "warning" + : "error" ) + .color ); + const String& text = doc->line( index ).getText(); + size_t minCol = text.find_first_not_of( " \t\f\v\n\r", match.pos.column() ); + if ( minCol == String::InvalidPos ) + minCol = match.pos.column(); + minCol = std::max( (Int64)minCol, match.pos.column() ); + if ( minCol >= text.size() ) + minCol = match.pos.column(); + if ( minCol >= text.size() ) + minCol = text.size() - 1; - std::string str( text.substr( minCol ).size() - 1, '~' ); - String string( str ); - line.setString( string ); + TextPosition endPos = doc->nextWordBoundary( { index, (Int64)minCol } ); + Int64 strSize = eemax( (Int64)0, static_cast( endPos.column() - minCol ) ); + std::string str( strSize, '~' ); + String string( str ); + line.setString( string ); - Vector2f pos( position.x + editor->getXOffsetCol( { match.pos.line(), (Int64)minCol } ), - position.y ); - Rectf box( pos - editor->getScreenPos(), { editor->getTextWidth( string ), lineHeight } ); - match.box = box; - line.draw( pos.x, pos.y + lineHeight * 0.5f ); + Vector2f pos( position.x + editor->getXOffsetCol( { match.pos.line(), (Int64)minCol } ), + position.y ); + Rectf box( pos - editor->getScreenPos(), { editor->getTextWidth( string ), lineHeight } ); + match.box = box; + line.draw( pos.x, pos.y + lineHeight * 0.5f ); + } } bool LinterModule::onMouseMove( UICodeEditor* editor, const Vector2i& pos, const Uint32& ) { @@ -335,15 +341,17 @@ bool LinterModule::onMouseMove( UICodeEditor* editor, const Vector2i& pos, const if ( it != mMatches.end() ) { Vector2f localPos( editor->convertToNodeSpace( pos.asFloat() ) ); for ( const auto& matchIt : it->second ) { - auto& match = matchIt.second; - if ( match.box.contains( localPos ) ) { - editor->setTooltipText( match.text ); - editor->getTooltip()->setPixelsPosition( Vector2f( pos.x, pos.y ) ); - editor->runOnMainThread( [&, editor] { editor->getTooltip()->show(); } ); - return false; - } else if ( editor->getTooltip() && editor->getTooltip()->isVisible() ) { - editor->setTooltipText( "" ); - editor->getTooltip()->hide(); + auto& matches = matchIt.second; + for ( const auto& match : matches ) { + if ( match.box.contains( localPos ) ) { + editor->setTooltipText( match.text ); + editor->getTooltip()->setPixelsPosition( Vector2f( pos.x, pos.y ) ); + editor->runOnMainThread( [&, editor] { editor->getTooltip()->show(); } ); + return false; + } else if ( editor->getTooltip() && editor->getTooltip()->isVisible() ) { + editor->setTooltipText( "" ); + editor->getTooltip()->hide(); + } } } } diff --git a/src/tools/codeeditor/lintermodule.hpp b/src/tools/codeeditor/lintermodule.hpp index c2408b9cb..6655bf563 100644 --- a/src/tools/codeeditor/lintermodule.hpp +++ b/src/tools/codeeditor/lintermodule.hpp @@ -63,7 +63,7 @@ class LinterModule : public UICodeEditorModule { std::set mDocs; std::unordered_map mEditorDocs; std::unordered_map> mDirtyDoc; - std::unordered_map> mMatches; + std::unordered_map>> mMatches; Time mDelayTime{ Seconds( 0.5f ) }; Mutex mDocMutex; Mutex mMatchesMutex;