ecode: Added Code Formatter module. Improved Linter module.

This commit is contained in:
Martín Lucas Golini
2021-01-15 02:29:26 -03:00
parent 92cb3ce12a
commit 239c5e00c6
12 changed files with 302 additions and 43 deletions

View File

@@ -0,0 +1,10 @@
[
{
"file_patterns": ["%.js$", "%.ts$"],
"command": "prettier $FILENAME"
},
{
"file_patterns": ["%.cpp$", "%.h$", "%.hpp$"],
"command": "clang-format --style=file $FILENAME"
}
]

View File

@@ -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,

View File

@@ -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

View File

@@ -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<size_t, size_t> findType( const String& str, const String& findStr,
const TextDocument::FindReplaceType& type ) {
switch ( type ) {

View File

@@ -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<std::string>& 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 );

View File

@@ -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{ "" };

View File

@@ -1,5 +1,6 @@
#include "codeeditor.hpp"
#include "autocompletemodule.hpp"
#include "formattermodule.hpp"
#include "lintermodule.hpp"
#include <algorithm>
#include <args/args.hxx>
@@ -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<UIMenuCheckBox>()->isActive() );
} else if ( item->getText() == "Enable Linter" ) {
setLinter( item->asType<UIMenuCheckBox>()->isActive() );
} else if ( item->getText() == "Enable Code Formatter" ) {
setFormatter( item->asType<UIMenuCheckBox>()->isActive() );
} else if ( item->getText() == "Enable Color Preview" ) {
mConfig.editor.colorPreview = item->asType<UIMenuCheckBox>()->isActive();
mEditorSplitter->forEachEditor( [&]( UICodeEditor* editor ) {
@@ -1100,7 +1107,8 @@ std::map<KeyBindings::Shortcut, std::string> 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<std::string> 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;

View File

@@ -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<ThreadPool> mThreadPool;
std::unique_ptr<ProjectDirectoryTree> 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 );

View File

@@ -0,0 +1,150 @@
#include "formattermodule.hpp"
#include "thirdparty/json.hpp"
#include "thirdparty/subprocess.h"
#include <eepp/system/filesystem.hpp>
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<ThreadPool> 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<std::string>() );
formatter.command = obj["command"].get<std::string>();
if ( obj.contains( "type" ) ) {
std::string typeStr( obj["type"].get<std::string>() );
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<TextDocument> 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<std::string> cmdArr = String::split( cmd, ' ' );
std::vector<const char*> 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<TextDocument> 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<TextDocument> 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 {};
}

View File

@@ -0,0 +1,46 @@
#ifndef FORMATTERMODULE_HPP
#define FORMATTERMODULE_HPP
#include <eepp/config.hpp>
#include <eepp/system/mutex.hpp>
#include <eepp/system/threadpool.hpp>
#include <eepp/ui/uicodeeditor.hpp>
#include <set>
using namespace EE;
using namespace EE::System;
using namespace EE::UI;
class FormatterModule : public UICodeEditorModule {
public:
FormatterModule( const std::string& formatterPath, std::shared_ptr<ThreadPool> pool );
virtual ~FormatterModule();
void onRegister( UICodeEditor* );
void onUnregister( UICodeEditor* );
protected:
enum class FormatterType { Inplace, Output };
struct Formatter {
std::vector<std::string> files;
std::string command;
FormatterType type{ FormatterType::Output };
};
std::shared_ptr<ThreadPool> mPool;
std::vector<Formatter> mFormatters;
std::set<UICodeEditor*> mEditors;
bool mClosing{ false };
bool mReady{ false };
void load( const std::string& formatterPath );
void formatDoc( UICodeEditor* editor );
FormatterModule::Formatter supportsFormatter( std::shared_ptr<TextDocument> doc );
};
#endif // FORMATTERMODULE_HPP

View File

@@ -176,6 +176,8 @@ static std::string randString( size_t len ) {
}
void LinterModule::lintDoc( std::shared_ptr<TextDocument> doc ) {
if ( !mReady )
return;
auto linter = supportsLinter( doc );
if ( linter.command.empty() )
return;
@@ -232,7 +234,7 @@ void LinterModule::runLinter( std::shared_ptr<TextDocument> doc, const Linter& l
// Log::info( "Linter result:\n%s", data.c_str() );
std::map<Int64, LinterMatch> matches;
std::map<Int64, std::vector<LinterMatch>> matches;
for ( auto& warningPatterm : linter.warningPattern ) {
LuaPattern pattern( warningPatterm );
@@ -265,7 +267,7 @@ void LinterModule::runLinter( std::shared_ptr<TextDocument> 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<Int64, LinterMatch>& map = matchIt->second;
std::map<Int64, std::vector<LinterMatch>>& 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<LinterMatch>& 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<Int64>( 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();
}
}
}
}

View File

@@ -63,7 +63,7 @@ class LinterModule : public UICodeEditorModule {
std::set<TextDocument*> mDocs;
std::unordered_map<UICodeEditor*, TextDocument*> mEditorDocs;
std::unordered_map<TextDocument*, std::unique_ptr<Clock>> mDirtyDoc;
std::unordered_map<TextDocument*, std::map<Int64, LinterMatch>> mMatches;
std::unordered_map<TextDocument*, std::map<Int64, std::vector<LinterMatch>>> mMatches;
Time mDelayTime{ Seconds( 0.5f ) };
Mutex mDocMutex;
Mutex mMatchesMutex;