mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-03 03:31:04 +03:00
- add a searchable Insert Snippet Universal Locator provider
- support runtime locator provider registration and removal - filter snippets with workspace-relative include/exclude globs - add the remaining VS Code-compatible snippet variables - support camelcase, pascalcase, snakecase, and kebabcase modifiers - reuse the existing insertion flow for locator-selected snippets - expand snippet parser and store test coverage - update the implementation plan and C/C++ example snippets Refs: SpartanJ/ecode#111
This commit is contained in:
@@ -113,11 +113,26 @@ UTEST( SnippetParser, variableTransforms ) {
|
||||
auto zeroWidth = SnippetParser::parse( R"(${TM_CURRENT_WORD/(?=.)/_/g})", variables );
|
||||
EXPECT_STDSTREQ( "_o_n_e_-_t_w_o", zeroWidth.text );
|
||||
variables["TM_CURRENT_WORD"] = "éx";
|
||||
auto unicodeZeroWidth =
|
||||
SnippetParser::parse( R"(${TM_CURRENT_WORD/(?=.)/_/g})", variables );
|
||||
auto unicodeZeroWidth = SnippetParser::parse( R"(${TM_CURRENT_WORD/(?=.)/_/g})", variables );
|
||||
EXPECT_STDSTREQ( "_é_x", unicodeZeroWidth.text );
|
||||
}
|
||||
|
||||
UTEST( SnippetParser, variableTransformCaseModifiers ) {
|
||||
SnippetParser::VariableMap variables{ { "TM_CURRENT_WORD", "hello-world value" } };
|
||||
EXPECT_STDSTREQ(
|
||||
"helloWorldValue",
|
||||
SnippetParser::parse( R"(${TM_CURRENT_WORD/(.*)/${1:/camelcase}/})", variables ).text );
|
||||
EXPECT_STDSTREQ(
|
||||
"HelloWorldValue",
|
||||
SnippetParser::parse( R"(${TM_CURRENT_WORD/(.*)/${1:/pascalcase}/})", variables ).text );
|
||||
EXPECT_STDSTREQ(
|
||||
"hello_world_value",
|
||||
SnippetParser::parse( R"(${TM_CURRENT_WORD/(.*)/${1:/snakecase}/})", variables ).text );
|
||||
EXPECT_STDSTREQ(
|
||||
"hello-world-value",
|
||||
SnippetParser::parse( R"(${TM_CURRENT_WORD/(.*)/${1:/kebabcase}/})", variables ).text );
|
||||
}
|
||||
|
||||
UTEST( SnippetParser, malformedPlaceholderRemainsLiteral ) {
|
||||
auto parsed = SnippetParser::parse( "before ${1:unfinished" );
|
||||
EXPECT_STDSTREQ( "before ${1:unfinished", parsed.text );
|
||||
|
||||
@@ -122,3 +122,56 @@ UTEST( UserSnippetStore, keepsLastGoodFileAndRemovesSources ) {
|
||||
EXPECT_TRUE( store.removeFile( "user.json" ) );
|
||||
EXPECT_EQ( 0u, store.size() );
|
||||
}
|
||||
|
||||
UTEST( UserSnippetStore, filtersByIncludedAndExcludedFilePatterns ) {
|
||||
UserSnippetStore store;
|
||||
ASSERT_TRUE( store.updateFile(
|
||||
R"json({
|
||||
"Tests": {
|
||||
"prefix": "testcase",
|
||||
"body": "test",
|
||||
"include": ["*_test.cpp", "tests/**/*.cpp"]
|
||||
},
|
||||
"Sources": {
|
||||
"prefix": "source",
|
||||
"body": "source",
|
||||
"exclude": ["generated/**", "*_test.cpp"]
|
||||
}
|
||||
})json",
|
||||
"cpp.code-snippets", UserSnippetSource::User ) );
|
||||
|
||||
auto testFile = store.find( "cpp", "", 10, "unit/example_test.cpp" );
|
||||
ASSERT_EQ( 1u, testFile.size() );
|
||||
EXPECT_STDSTREQ( "Tests", testFile[0].snippet.name );
|
||||
|
||||
auto nestedTest =
|
||||
store.findForLocator( "cpp", "test", 10, "tests\\unit\\example.cpp" );
|
||||
ASSERT_EQ( 1u, nestedTest.size() );
|
||||
EXPECT_STDSTREQ( "Tests", nestedTest[0].snippet.name );
|
||||
|
||||
auto sourceFile = store.find( "cpp", "", 10, "src/example.cpp" );
|
||||
ASSERT_EQ( 1u, sourceFile.size() );
|
||||
EXPECT_STDSTREQ( "Sources", sourceFile[0].snippet.name );
|
||||
EXPECT_TRUE( store.find( "cpp", "", 10, "generated/example.cpp" ).empty() );
|
||||
auto nestedVendorTest = store.find( "cpp", "", 10, "vendor/tests/unit/example.cpp" );
|
||||
ASSERT_EQ( 1u, nestedVendorTest.size() );
|
||||
EXPECT_STDSTREQ( "Sources", nestedVendorTest[0].snippet.name );
|
||||
}
|
||||
|
||||
UTEST( UserSnippetStore, locatorSearchesNamesPrefixesAndDescriptions ) {
|
||||
UserSnippetStore store;
|
||||
ASSERT_TRUE( store.updateFile(
|
||||
R"json({
|
||||
"Indexed Loop": {
|
||||
"prefix": ["fori", "for-index"],
|
||||
"body": "loop",
|
||||
"description": "Iterate over values"
|
||||
}
|
||||
})json",
|
||||
"cpp.json", UserSnippetSource::User, "cpp" ) );
|
||||
|
||||
EXPECT_EQ( 1u, store.findForLocator( "cpp", "Indexed", 10 ).size() );
|
||||
EXPECT_EQ( 1u, store.findForLocator( "cpp", "fori", 10 ).size() );
|
||||
EXPECT_EQ( 1u, store.findForLocator( "cpp", "values", 10 ).size() );
|
||||
EXPECT_TRUE( store.findForLocator( "rust", "Indexed", 10 ).empty() );
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
#include "autocompleteplugin.hpp"
|
||||
#include "../../universallocator.hpp"
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/math/math.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/lock.hpp>
|
||||
#include <eepp/system/luapattern.hpp>
|
||||
#include <eepp/system/scopedop.hpp>
|
||||
#include <eepp/system/uuid.hpp>
|
||||
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
|
||||
#include <eepp/ui/uieventdispatcher.hpp>
|
||||
#include <eepp/ui/uiplacementutils.hpp>
|
||||
#include <eepp/ui/uipopupmenu.hpp>
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string_view>
|
||||
using namespace EE::Graphics;
|
||||
@@ -21,6 +26,69 @@ using namespace std::literals;
|
||||
|
||||
namespace ecode {
|
||||
|
||||
class SnippetLocatorModel : public Model {
|
||||
public:
|
||||
struct Row {
|
||||
std::string name;
|
||||
std::string prefixes;
|
||||
std::string detail;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
explicit SnippetLocatorModel( std::vector<UserSnippetMatch> matches ) {
|
||||
mRows.reserve( matches.size() );
|
||||
for ( auto& match : matches ) {
|
||||
std::string prefixes;
|
||||
for ( const auto& prefix : match.snippet.prefixes ) {
|
||||
if ( !prefixes.empty() )
|
||||
prefixes += ", ";
|
||||
prefixes += prefix;
|
||||
}
|
||||
const char* source = match.snippet.source == UserSnippetSource::EcodeProject ? ".ecode"
|
||||
: match.snippet.source == UserSnippetSource::VSCodeProject
|
||||
? ".vscode"
|
||||
: "user";
|
||||
std::string detail( std::move( match.snippet.description ) );
|
||||
if ( !detail.empty() )
|
||||
detail += " — ";
|
||||
detail += source;
|
||||
mRows.push_back( { std::move( match.snippet.name ), std::move( prefixes ),
|
||||
std::move( detail ), std::move( match.snippet.body ) } );
|
||||
}
|
||||
}
|
||||
|
||||
size_t rowCount( const ModelIndex& ) const override { return mRows.size(); }
|
||||
|
||||
size_t columnCount( const ModelIndex& ) const override { return 3; }
|
||||
|
||||
std::string columnName( const size_t& column ) const override {
|
||||
static constexpr std::array<std::string_view, 3> names{ "Name", "Prefixes", "Description" };
|
||||
return std::string{ names[column] };
|
||||
}
|
||||
|
||||
Variant data( const ModelIndex& index, ModelRole role = ModelRole::Display ) const override {
|
||||
if ( !index.isValid() || index.row() >= static_cast<Int64>( mRows.size() ) )
|
||||
return {};
|
||||
const auto& row = mRows[index.row()];
|
||||
if ( role == ModelRole::Custom )
|
||||
return Variant{ row.body };
|
||||
if ( role != ModelRole::Display )
|
||||
return {};
|
||||
switch ( index.column() ) {
|
||||
case 0:
|
||||
return Variant{ row.name };
|
||||
case 1:
|
||||
return Variant{ row.prefixes };
|
||||
case 2:
|
||||
return Variant{ row.detail };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<Row> mRows;
|
||||
};
|
||||
|
||||
static bool pathStartsWith( std::string_view path, std::string_view prefix ) {
|
||||
return !prefix.empty() && path.size() >= prefix.size() &&
|
||||
path.compare( 0, prefix.size(), prefix ) == 0;
|
||||
@@ -199,6 +267,7 @@ AutoCompletePlugin::AutoCompletePlugin( PluginManager* pluginManager, bool sync
|
||||
AutoCompletePlugin::~AutoCompletePlugin() {
|
||||
waitUntilLoaded();
|
||||
mShuttingDown = true;
|
||||
unregisterSnippetLocatorProvider();
|
||||
mManager->unsubscribeMessages( this );
|
||||
unsubscribeFileSystemListener();
|
||||
while ( mSnippetJobs > 0 )
|
||||
@@ -484,6 +553,67 @@ void AutoCompletePlugin::onLoadProject( const std::string& projectFolder,
|
||||
setSnippetWorkspaceFolder( projectFolder );
|
||||
}
|
||||
|
||||
void AutoCompletePlugin::registerSnippetLocatorProvider() {
|
||||
if ( mSnippetLocatorProviderId != 0 || !getPluginContext() ||
|
||||
!getPluginContext()->getUniversalLocator() )
|
||||
return;
|
||||
auto* locator = getPluginContext()->getUniversalLocator();
|
||||
mSnippetLocatorProviderId = locator->registerLocatorProvider(
|
||||
{ "sn", i18n( "insert_snippet", "Insert Snippet" ), nullptr,
|
||||
[this, locator]( const Variant&, const ModelEvent* event ) {
|
||||
if ( !event || !event->getModel() || !getPluginContext() )
|
||||
return;
|
||||
auto* editor = getPluginContext()->getSplitter()->getCurEditor();
|
||||
if ( !editor )
|
||||
return;
|
||||
const auto body = event->getModel()->data(
|
||||
event->getModel()->index( event->getModelIndex().row(), 0 ), ModelRole::Custom );
|
||||
if ( !body.isValid() )
|
||||
return;
|
||||
mReplacing = true;
|
||||
insertSnippet( editor, body.toString() );
|
||||
mReplacing = false;
|
||||
resetSuggestions( editor );
|
||||
locator->hideLocateBar();
|
||||
editor->setFocus();
|
||||
},
|
||||
nullptr, false, false,
|
||||
[this]( const String& query, UniversalLocator::LocatorProvider::ModelReadyFn ready ) {
|
||||
if ( !getPluginContext() || !getPluginContext()->getSplitter() ) {
|
||||
ready( std::make_shared<SnippetLocatorModel>( std::vector<UserSnippetMatch>{} ) );
|
||||
return;
|
||||
}
|
||||
auto* editor = getPluginContext()->getSplitter()->getCurEditor();
|
||||
if ( !editor ) {
|
||||
ready( std::make_shared<SnippetLocatorModel>( std::vector<UserSnippetMatch>{} ) );
|
||||
return;
|
||||
}
|
||||
std::string language = editor->getDocument().getSyntaxDefinition().getLSPName();
|
||||
std::string filePath = editor->getDocument().getFilePath();
|
||||
FileSystem::filePathRemoveBasePath( getPluginContext()->getCurrentProject(), filePath );
|
||||
std::string pattern = query.toUtf8();
|
||||
++mSnippetJobs;
|
||||
mThreadPool->run( [this, language = std::move( language ),
|
||||
filePath = std::move( filePath ), pattern = std::move( pattern ),
|
||||
ready = std::move( ready )] {
|
||||
ScopedOp job( [] {}, [this] { --mSnippetJobs; } );
|
||||
if ( mShuttingDown )
|
||||
return;
|
||||
ready( std::make_shared<SnippetLocatorModel>(
|
||||
mUserSnippetStore.findForLocator( language, pattern, 100, filePath ) ) );
|
||||
} );
|
||||
} } );
|
||||
}
|
||||
|
||||
void AutoCompletePlugin::unregisterSnippetLocatorProvider() {
|
||||
if ( mSnippetLocatorProviderId == 0 || !getPluginContext() ||
|
||||
!getPluginContext()->getUniversalLocator() )
|
||||
return;
|
||||
getPluginContext()->getUniversalLocator()->unregisterLocatorProvider(
|
||||
mSnippetLocatorProviderId );
|
||||
mSnippetLocatorProviderId = 0;
|
||||
}
|
||||
|
||||
void AutoCompletePlugin::onFileSystemEvent( const FileEvent& ev, const FileInfo& file ) {
|
||||
Plugin::onFileSystemEvent( ev, file );
|
||||
if ( mShuttingDown || isLoading() )
|
||||
@@ -536,6 +666,7 @@ void AutoCompletePlugin::onFileSystemEvent( const FileEvent& ev, const FileInfo&
|
||||
}
|
||||
|
||||
void AutoCompletePlugin::onRegister( UICodeEditor* editor ) {
|
||||
registerSnippetLocatorProvider();
|
||||
Lock l( mDocMutex );
|
||||
std::vector<Uint32> listeners;
|
||||
listeners.push_back( editor->on( Event::OnDocumentLoaded, [this, editor]( const Event* ) {
|
||||
@@ -965,22 +1096,83 @@ void AutoCompletePlugin::updateLangCache( const std::string& langName ) {
|
||||
clock.getElapsedTime().asMilliseconds() );
|
||||
}
|
||||
|
||||
static SnippetParser::VariableMap snippetVariables( TextDocument& doc,
|
||||
const TextRange& selection ) {
|
||||
static std::string formatSnippetTime( const std::tm& time, const char* format ) {
|
||||
char buffer[128];
|
||||
return std::strftime( buffer, sizeof( buffer ), format, &time ) > 0 ? buffer : "";
|
||||
}
|
||||
|
||||
SnippetParser::VariableMap AutoCompletePlugin::snippetVariables( TextDocument& doc,
|
||||
const TextRange& selection,
|
||||
size_t cursorIndex ) const {
|
||||
const TextPosition position = selection.normalized().start();
|
||||
std::string filePath = doc.getFilePath();
|
||||
if ( filePath.empty() )
|
||||
filePath = doc.getLoadingFilePath();
|
||||
const std::string filename = FileSystem::fileNameFromPath( filePath );
|
||||
return { { "TM_SELECTED_TEXT", doc.getText( selection ).toUtf8() },
|
||||
{ "TM_CURRENT_LINE", doc.getLineTextWithoutNewLine( position.line() ).toUtf8() },
|
||||
{ "TM_CURRENT_WORD", doc.getWordInPosition( position ).toUtf8() },
|
||||
{ "TM_LINE_INDEX", String::toString( position.line() ) },
|
||||
{ "TM_LINE_NUMBER", String::toString( position.line() + 1 ) },
|
||||
{ "TM_FILENAME", filename },
|
||||
{ "TM_FILENAME_BASE", FileSystem::fileRemoveExtension( filename ) },
|
||||
{ "TM_DIRECTORY", FileSystem::fileRemoveFileName( filePath ) },
|
||||
{ "TM_FILEPATH", filePath } };
|
||||
std::string workspaceFolder = getPluginContext() ? getPluginContext()->getCurrentProject() : "";
|
||||
std::string relativeFilePath( filePath );
|
||||
if ( !workspaceFolder.empty() )
|
||||
FileSystem::filePathRemoveBasePath( workspaceFolder, relativeFilePath );
|
||||
FileSystem::dirRemoveSlashAtEnd( workspaceFolder );
|
||||
std::string workspaceName( FileSystem::fileNameFromPath( workspaceFolder ) );
|
||||
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto nowTime = std::chrono::system_clock::to_time_t( now );
|
||||
const std::tm* localTimePtr = std::localtime( &nowTime );
|
||||
const std::tm localTime = localTimePtr ? *localTimePtr : std::tm{};
|
||||
const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch() );
|
||||
std::string timezoneOffset = formatSnippetTime( localTime, "%z" );
|
||||
if ( timezoneOffset.size() == 5 )
|
||||
timezoneOffset.insert( 3, ":" );
|
||||
|
||||
const auto& syntax = doc.getSyntaxDefinition();
|
||||
const auto& blockComment = syntax.getBlockComment();
|
||||
SnippetParser::VariableMap variables{
|
||||
{ "TM_SELECTED_TEXT", doc.getText( selection ).toUtf8() },
|
||||
{ "TM_CURRENT_LINE", doc.getLineTextWithoutNewLine( position.line() ).toUtf8() },
|
||||
{ "TM_CURRENT_WORD", doc.getWordInPosition( position ).toUtf8() },
|
||||
{ "TM_LINE_INDEX", String::toString( position.line() ) },
|
||||
{ "TM_LINE_NUMBER", String::toString( position.line() + 1 ) },
|
||||
{ "TM_FILENAME", filename },
|
||||
{ "TM_FILENAME_BASE", FileSystem::fileRemoveExtension( filename ) },
|
||||
{ "TM_DIRECTORY", FileSystem::fileRemoveFileName( filePath ) },
|
||||
{ "TM_FILEPATH", filePath },
|
||||
{ "RELATIVE_FILEPATH", relativeFilePath },
|
||||
{ "WORKSPACE_NAME", workspaceName },
|
||||
{ "WORKSPACE_FOLDER", workspaceFolder },
|
||||
{ "CLIPBOARD", getUISceneNode() && getUISceneNode()->getWindow()
|
||||
? getUISceneNode()->getWindow()->getClipboard()->getText()
|
||||
: "" },
|
||||
{ "CURSOR_INDEX", String::toString( static_cast<Uint64>( cursorIndex ) ) },
|
||||
{ "CURSOR_NUMBER", String::toString( static_cast<Uint64>( cursorIndex + 1 ) ) },
|
||||
{ "CURRENT_YEAR", formatSnippetTime( localTime, "%Y" ) },
|
||||
{ "CURRENT_YEAR_SHORT", formatSnippetTime( localTime, "%y" ) },
|
||||
{ "CURRENT_MONTH", formatSnippetTime( localTime, "%m" ) },
|
||||
{ "CURRENT_MONTH_NAME", formatSnippetTime( localTime, "%B" ) },
|
||||
{ "CURRENT_MONTH_NAME_SHORT", formatSnippetTime( localTime, "%b" ) },
|
||||
{ "CURRENT_DATE", formatSnippetTime( localTime, "%d" ) },
|
||||
{ "CURRENT_DAY_NAME", formatSnippetTime( localTime, "%A" ) },
|
||||
{ "CURRENT_DAY_NAME_SHORT", formatSnippetTime( localTime, "%a" ) },
|
||||
{ "CURRENT_HOUR", formatSnippetTime( localTime, "%H" ) },
|
||||
{ "CURRENT_MINUTE", formatSnippetTime( localTime, "%M" ) },
|
||||
{ "CURRENT_SECOND", formatSnippetTime( localTime, "%S" ) },
|
||||
{ "CURRENT_MILLISECOND",
|
||||
String::format( "%03d", static_cast<int>( milliseconds.count() % 1000 ) ) },
|
||||
{ "CURRENT_SECONDS_UNIX",
|
||||
String::toString( static_cast<Int64>( milliseconds.count() / 1000 ) ) },
|
||||
{ "CURRENT_MILLISECONDS_UNIX",
|
||||
String::toString( static_cast<Int64>( milliseconds.count() ) ) },
|
||||
{ "CURRENT_TIMEZONE_OFFSET", timezoneOffset },
|
||||
{ "CURRENT_TIMEZONE_NAME", formatSnippetTime( localTime, "%Z" ) },
|
||||
{ "RANDOM", String::format( "%06d", Math::randi( 0, 999999 ) ) },
|
||||
{ "RANDOM_HEX", String::format( "%06x", Math::randi( 0, 0xFFFFFF ) ) },
|
||||
{ "UUID", UUID().toString() },
|
||||
{ "LINE_COMMENT", syntax.getComment() },
|
||||
{ "BLOCK_COMMENT_START", blockComment.open },
|
||||
{ "BLOCK_COMMENT_END", blockComment.close },
|
||||
};
|
||||
return variables;
|
||||
}
|
||||
|
||||
static std::string prepareSnippetText( TextDocument& doc, const TextRange& selection,
|
||||
@@ -1034,6 +1226,47 @@ static TextRange userSnippetActivationRange( TextDocument& doc, const TextRange&
|
||||
return selection;
|
||||
}
|
||||
|
||||
void AutoCompletePlugin::insertSnippet( UICodeEditor* editor, std::string_view body,
|
||||
const Suggestion* suggestion ) {
|
||||
auto doc = editor->getDocumentRef();
|
||||
auto prevSels = doc->getSelections();
|
||||
std::vector<SnippetInsertion> insertions;
|
||||
insertions.reserve( prevSels.size() );
|
||||
for ( size_t index = 0; index < prevSels.size(); ++index ) {
|
||||
const auto& selection = prevSels[index];
|
||||
const std::string prepared = prepareSnippetText( *doc, selection, body );
|
||||
insertions.push_back(
|
||||
{ SnippetParser::parse( prepared, snippetVariables( *doc, selection, index ) ), {} } );
|
||||
}
|
||||
|
||||
if ( suggestion && suggestion->source == Suggestion::Source::UserSnippet ) {
|
||||
const std::string symbol( getPartialSymbol( doc.get() ) );
|
||||
for ( size_t index = 0; index < prevSels.size(); ++index )
|
||||
doc->setSelection( index,
|
||||
userSnippetActivationRange( *doc, prevSels[index],
|
||||
suggestion->matchedPrefix, symbol ) );
|
||||
} else if ( suggestion && prevSels.size() == 1 && suggestion->range.isValid() &&
|
||||
doc->isValidRange( suggestion->range ) ) {
|
||||
doc->setSelection( suggestion->range );
|
||||
} else if ( suggestion ) {
|
||||
const std::string symbol( getPartialSymbol( doc.get() ) );
|
||||
if ( !symbol.empty() )
|
||||
doc->execute( "delete-to-previous-word" );
|
||||
}
|
||||
if ( insertions.size() > doc->getSelections().size() )
|
||||
insertions.resize( doc->getSelections().size() );
|
||||
|
||||
for ( size_t index = 0; index < insertions.size(); ++index ) {
|
||||
if ( doc->getSelectionIndex( index ).hasSelection() )
|
||||
doc->deleteTo( index, 0 );
|
||||
insertions[index].start = doc->getSelectionIndex( index ).start();
|
||||
TextPosition end = doc->insert( index, insertions[index].start,
|
||||
String::fromUtf8( insertions[index].snippet.text ) );
|
||||
doc->setSelection( index, end );
|
||||
}
|
||||
tryStartSnippetNav( insertions, editor );
|
||||
}
|
||||
|
||||
void AutoCompletePlugin::pickSuggestion( UICodeEditor* editor ) {
|
||||
if ( mSnippetChoiceSuggestions )
|
||||
return pickSnippetChoice( editor );
|
||||
@@ -1057,37 +1290,7 @@ void AutoCompletePlugin::pickSuggestion( UICodeEditor* editor ) {
|
||||
doc->textInput( rawInsertText );
|
||||
}
|
||||
} else {
|
||||
std::vector<SnippetInsertion> insertions;
|
||||
insertions.reserve( prevSels.size() );
|
||||
for ( const auto& selection : prevSels ) {
|
||||
const std::string prepared = prepareSnippetText( *doc, selection, rawInsertText );
|
||||
insertions.push_back(
|
||||
{ SnippetParser::parse( prepared, snippetVariables( *doc, selection ) ), {} } );
|
||||
}
|
||||
|
||||
if ( suggestion.source == Suggestion::Source::UserSnippet ) {
|
||||
for ( size_t index = 0; index < prevSels.size(); ++index )
|
||||
doc->setSelection( index,
|
||||
userSnippetActivationRange( *doc, prevSels[index],
|
||||
suggestion.matchedPrefix, symbol ) );
|
||||
} else if ( prevSels.size() == 1 && suggestion.range.isValid() &&
|
||||
doc->isValidRange( suggestion.range ) ) {
|
||||
doc->setSelection( suggestion.range );
|
||||
} else if ( !symbol.empty() ) {
|
||||
doc->execute( "delete-to-previous-word" );
|
||||
}
|
||||
if ( insertions.size() > doc->getSelections().size() )
|
||||
insertions.resize( doc->getSelections().size() );
|
||||
|
||||
for ( size_t index = 0; index < insertions.size(); ++index ) {
|
||||
if ( doc->getSelectionIndex( index ).hasSelection() )
|
||||
doc->deleteTo( index, 0 );
|
||||
insertions[index].start = doc->getSelectionIndex( index ).start();
|
||||
TextPosition end = doc->insert( index, insertions[index].start,
|
||||
String::fromUtf8( insertions[index].snippet.text ) );
|
||||
doc->setSelection( index, end );
|
||||
}
|
||||
tryStartSnippetNav( insertions, editor );
|
||||
insertSnippet( editor, rawInsertText, &suggestion );
|
||||
}
|
||||
|
||||
mReplacing = false;
|
||||
@@ -2381,7 +2584,11 @@ AutoCompletePlugin::getUserSnippetSuggestions( UICodeEditor* editor, const std::
|
||||
std::string snippetInput = getUserSnippetInput( editor );
|
||||
if ( snippetInput.empty() )
|
||||
snippetInput = symbol;
|
||||
auto matches = mUserSnippetStore.find( language, snippetInput, maxResults );
|
||||
std::string filePath = editor->getDocument().getFilePath();
|
||||
if ( getPluginContext() )
|
||||
FileSystem::filePathRemoveBasePath( getPluginContext()->getCurrentProject(), filePath );
|
||||
auto matches = mUserSnippetStore.find( language, snippetInput, maxResults,
|
||||
filePath );
|
||||
suggestions.reserve( matches.size() );
|
||||
for ( auto& match : matches ) {
|
||||
Suggestion suggestion( LSPCompletionItemKind::Snippet, std::move( match.matchedPrefix ),
|
||||
|
||||
@@ -240,6 +240,7 @@ class AutoCompletePlugin : public Plugin {
|
||||
std::string mSnippetEventPathBuffer;
|
||||
std::atomic<Uint64> mSnippetWorkspaceGeneration{ 0 };
|
||||
std::atomic<Uint32> mSnippetJobs{ 0 };
|
||||
Uint64 mSnippetLocatorProviderId{ 0 };
|
||||
|
||||
explicit AutoCompletePlugin( PluginManager* pluginManager, bool sync );
|
||||
|
||||
@@ -273,6 +274,16 @@ class AutoCompletePlugin : public Plugin {
|
||||
void scheduleSnippetFileUpdate( std::string path, UserSnippetSource source, bool languageFiles,
|
||||
bool remove );
|
||||
|
||||
void registerSnippetLocatorProvider();
|
||||
|
||||
void unregisterSnippetLocatorProvider();
|
||||
|
||||
void insertSnippet( UICodeEditor* editor, std::string_view body,
|
||||
const Suggestion* suggestion = nullptr );
|
||||
|
||||
SnippetParser::VariableMap snippetVariables( TextDocument& doc, const TextRange& selection,
|
||||
size_t cursorIndex ) const;
|
||||
|
||||
void updateLangCache( const std::string& langName );
|
||||
|
||||
void pickSuggestion( UICodeEditor* editor );
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "snippetparser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <eepp/core/string.hpp>
|
||||
#include <eepp/system/regex.hpp>
|
||||
#include <limits>
|
||||
@@ -203,6 +204,43 @@ class Parser {
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<std::string> caseWords( std::string_view value ) {
|
||||
std::vector<std::string> words;
|
||||
size_t start = 0;
|
||||
for ( size_t pos = 0; pos <= value.size(); ++pos ) {
|
||||
const bool atEnd = pos == value.size();
|
||||
const Uint8 current = atEnd ? 0 : static_cast<Uint8>( value[pos] );
|
||||
const bool separator = !atEnd && current < 0x80 && !std::isalnum( current );
|
||||
const bool caseBoundary = !atEnd && pos > start && current < 0x80 &&
|
||||
std::isupper( current ) &&
|
||||
static_cast<Uint8>( value[pos - 1] ) < 0x80 &&
|
||||
std::islower( static_cast<Uint8>( value[pos - 1] ) );
|
||||
if ( atEnd || separator || caseBoundary ) {
|
||||
if ( pos > start )
|
||||
words.emplace_back(
|
||||
String::toLower( std::string{ value.substr( start, pos - start ) } ) );
|
||||
start = separator ? pos + 1 : pos;
|
||||
}
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
static std::string transformCase( std::string_view value, std::string_view modifier ) {
|
||||
auto words = caseWords( value );
|
||||
std::string output;
|
||||
for ( size_t index = 0; index < words.size(); ++index ) {
|
||||
if ( modifier == "/snakecase" && index > 0 )
|
||||
output += '_';
|
||||
else if ( modifier == "/kebabcase" && index > 0 )
|
||||
output += '-';
|
||||
if ( modifier == "/pascalcase" || ( modifier == "/camelcase" && index > 0 ) )
|
||||
output += String::capitalize( words[index] );
|
||||
else
|
||||
output += words[index];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static std::string transformFormat( std::string_view format, std::string_view value,
|
||||
const std::vector<PatternMatcher::Range>& matches ) {
|
||||
std::string output;
|
||||
@@ -258,6 +296,9 @@ class Parser {
|
||||
output += String::toLower( group );
|
||||
else if ( body == "/capitalize" )
|
||||
output += String::capitalize( group );
|
||||
else if ( body == "/camelcase" || body == "/pascalcase" || body == "/snakecase" ||
|
||||
body == "/kebabcase" )
|
||||
output += transformCase( group, body );
|
||||
else if ( String::startsWith( body, "+" ) )
|
||||
output += !group.empty() ? unescapeFormatText( body.substr( 1 ) ) : "";
|
||||
else if ( String::startsWith( body, "?" ) ) {
|
||||
|
||||
@@ -61,6 +61,11 @@ static bool parseStringList( const json& value, SmallVector<std::string, 2>& str
|
||||
return !strings.empty() || !rejectEmpty;
|
||||
}
|
||||
|
||||
static void normalizeFilePatterns( SmallVector<std::string, 2>& patterns ) {
|
||||
for ( auto& pattern : patterns )
|
||||
String::replaceAll( pattern, "\\", "/" );
|
||||
}
|
||||
|
||||
static bool parseBody( const json& value, std::string& body ) {
|
||||
if ( value.is_string() ) {
|
||||
body = value.get<std::string>();
|
||||
@@ -92,6 +97,24 @@ static int sourcePriority( UserSnippetSource source ) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool matchesFile( const UserSnippetDefinition& snippet, std::string_view filePath ) {
|
||||
if ( !snippet.includePatterns.empty() ) {
|
||||
bool included = false;
|
||||
for ( const auto& pattern : snippet.includePatterns ) {
|
||||
if ( String::globMatch( filePath, pattern ) ) {
|
||||
included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !included )
|
||||
return false;
|
||||
}
|
||||
for ( const auto& pattern : snippet.excludePatterns )
|
||||
if ( String::globMatch( filePath, pattern ) )
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UserSnippetParseResult UserSnippetStore::parseFile( std::string_view contents,
|
||||
@@ -138,6 +161,20 @@ UserSnippetParseResult UserSnippetStore::parseFile( std::string_view contents,
|
||||
continue;
|
||||
}
|
||||
snippet.scopes = parseScopes( value, defaultScope );
|
||||
if ( value.contains( "include" ) &&
|
||||
!parseStringList( value["include"], snippet.includePatterns, true ) ) {
|
||||
result.diagnostics.emplace_back( sourcePath + ": snippet '" + name +
|
||||
"' has an invalid include pattern" );
|
||||
continue;
|
||||
}
|
||||
if ( value.contains( "exclude" ) &&
|
||||
!parseStringList( value["exclude"], snippet.excludePatterns, true ) ) {
|
||||
result.diagnostics.emplace_back( sourcePath + ": snippet '" + name +
|
||||
"' has an invalid exclude pattern" );
|
||||
continue;
|
||||
}
|
||||
normalizeFilePatterns( snippet.includePatterns );
|
||||
normalizeFilePatterns( snippet.excludePatterns );
|
||||
result.snippets.emplace_back( std::move( snippet ) );
|
||||
}
|
||||
return result;
|
||||
@@ -220,8 +257,8 @@ void UserSnippetStore::rebuildSnapshot() {
|
||||
}
|
||||
|
||||
std::vector<UserSnippetMatch> UserSnippetStore::find( std::string_view language,
|
||||
std::string_view pattern,
|
||||
size_t maxResults ) const {
|
||||
std::string_view pattern, size_t maxResults,
|
||||
std::string_view filePath ) const {
|
||||
if ( maxResults == 0 )
|
||||
return {};
|
||||
std::shared_ptr<const Snapshot> snapshot;
|
||||
@@ -230,6 +267,8 @@ std::vector<UserSnippetMatch> UserSnippetStore::find( std::string_view language,
|
||||
snapshot = mSnapshot;
|
||||
}
|
||||
std::string normalizedLanguage( language );
|
||||
std::string normalizedPath( filePath );
|
||||
String::replaceAll( normalizedPath, "\\", "/" );
|
||||
String::toLowerInPlace( normalizedLanguage );
|
||||
std::vector<size_t> candidates;
|
||||
candidates.reserve( snapshot->global.size() + 32 );
|
||||
@@ -263,6 +302,8 @@ std::vector<UserSnippetMatch> UserSnippetStore::find( std::string_view language,
|
||||
matchedCandidates.reserve( eemin( maxResults, candidates.size() ) );
|
||||
for ( size_t index : candidates ) {
|
||||
const auto& snippet = snapshot->snippets[index];
|
||||
if ( !matchesFile( snippet, normalizedPath ) )
|
||||
continue;
|
||||
int bestScore = std::numeric_limits<int>::min();
|
||||
size_t bestPrefix = NO_INPUT;
|
||||
size_t bestInput = NO_INPUT;
|
||||
@@ -320,6 +361,76 @@ std::vector<UserSnippetMatch> UserSnippetStore::find( std::string_view language,
|
||||
return matches;
|
||||
}
|
||||
|
||||
std::vector<UserSnippetMatch> UserSnippetStore::findForLocator( std::string_view language,
|
||||
std::string_view pattern,
|
||||
size_t maxResults,
|
||||
std::string_view filePath ) const {
|
||||
if ( maxResults == 0 )
|
||||
return {};
|
||||
std::shared_ptr<const Snapshot> snapshot;
|
||||
{
|
||||
Lock lock( mMutex );
|
||||
snapshot = mSnapshot;
|
||||
}
|
||||
std::string normalizedLanguage( language );
|
||||
const std::string query( pattern );
|
||||
std::string normalizedPath( filePath );
|
||||
String::replaceAll( normalizedPath, "\\", "/" );
|
||||
String::toLowerInPlace( normalizedLanguage );
|
||||
std::vector<size_t> candidates;
|
||||
candidates.reserve( snapshot->global.size() + 32 );
|
||||
candidates.insert( candidates.end(), snapshot->global.begin(), snapshot->global.end() );
|
||||
auto languageIt = snapshot->byLanguage.find( normalizedLanguage );
|
||||
if ( languageIt != snapshot->byLanguage.end() )
|
||||
candidates.insert( candidates.end(), languageIt->second.begin(), languageIt->second.end() );
|
||||
|
||||
struct Candidate {
|
||||
size_t snippetIndex;
|
||||
int score;
|
||||
};
|
||||
std::vector<Candidate> matches;
|
||||
matches.reserve( eemin( maxResults, candidates.size() ) );
|
||||
const auto matchScore = [&query]( const std::string& value ) {
|
||||
const int score = String::fuzzyMatchSimple( query, value, false, true );
|
||||
return score > 0 ? score : String::icontains( value, query ) ? 1 : 0;
|
||||
};
|
||||
for ( size_t index : candidates ) {
|
||||
const auto& snippet = snapshot->snippets[index];
|
||||
if ( !matchesFile( snippet, normalizedPath ) )
|
||||
continue;
|
||||
int score = query.empty() ? 0 : matchScore( snippet.name );
|
||||
if ( !pattern.empty() ) {
|
||||
for ( const auto& prefix : snippet.prefixes )
|
||||
score = eemax( score, matchScore( prefix ) );
|
||||
if ( !snippet.description.empty() )
|
||||
score = eemax( score, matchScore( snippet.description ) );
|
||||
}
|
||||
if ( pattern.empty() || score > 0 )
|
||||
matches.push_back( { index, score } );
|
||||
}
|
||||
std::sort( matches.begin(), matches.end(),
|
||||
[&]( const Candidate& left, const Candidate& right ) {
|
||||
if ( left.score != right.score )
|
||||
return left.score > right.score;
|
||||
const auto& leftSnippet = snapshot->snippets[left.snippetIndex];
|
||||
const auto& rightSnippet = snapshot->snippets[right.snippetIndex];
|
||||
const int leftPriority = sourcePriority( leftSnippet.source );
|
||||
const int rightPriority = sourcePriority( rightSnippet.source );
|
||||
if ( leftPriority != rightPriority )
|
||||
return leftPriority > rightPriority;
|
||||
return leftSnippet.name < rightSnippet.name;
|
||||
} );
|
||||
if ( matches.size() > maxResults )
|
||||
matches.resize( maxResults );
|
||||
std::vector<UserSnippetMatch> results;
|
||||
results.reserve( matches.size() );
|
||||
for ( const auto& match : matches ) {
|
||||
const auto& snippet = snapshot->snippets[match.snippetIndex];
|
||||
results.push_back( { snippet, snippet.prefixes.front(), {}, match.score } );
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
size_t UserSnippetStore::size() const {
|
||||
Lock lock( mMutex );
|
||||
return mSnapshot->snippets.size();
|
||||
|
||||
@@ -24,6 +24,8 @@ struct UserSnippetDefinition {
|
||||
std::string body;
|
||||
std::string description;
|
||||
SmallVector<std::string, 2> scopes;
|
||||
SmallVector<std::string, 2> includePatterns;
|
||||
SmallVector<std::string, 2> excludePatterns;
|
||||
std::string sourcePath;
|
||||
UserSnippetSource source{ UserSnippetSource::User };
|
||||
};
|
||||
@@ -58,7 +60,11 @@ class UserSnippetStore {
|
||||
void clear();
|
||||
|
||||
std::vector<UserSnippetMatch> find( std::string_view language, std::string_view pattern,
|
||||
size_t maxResults ) const;
|
||||
size_t maxResults, std::string_view filePath = {} ) const;
|
||||
|
||||
std::vector<UserSnippetMatch> findForLocator( std::string_view language,
|
||||
std::string_view pattern, size_t maxResults,
|
||||
std::string_view filePath = {} ) const;
|
||||
|
||||
size_t size() const;
|
||||
|
||||
|
||||
@@ -431,6 +431,7 @@ UniversalLocator::UniversalLocator( UICodeEditorSplitter* editorSplitter, UIScen
|
||||
}
|
||||
|
||||
void UniversalLocator::hideLocateBar() {
|
||||
++mLocatorModelGeneration;
|
||||
mLocateBarLayout->setVisible( false );
|
||||
mLocateTable->setVisible( false );
|
||||
mApp->getStatusBar()->updateState();
|
||||
@@ -535,6 +536,7 @@ void UniversalLocator::goToLine() {
|
||||
}
|
||||
|
||||
bool UniversalLocator::isCommand( const std::string& filename ) {
|
||||
Lock lock( mLocatorProvidersMutex );
|
||||
const auto isLocator = [this]( const std::string& filename ) {
|
||||
return std::find_if( mLocatorProviders.begin(), mLocatorProviders.end(),
|
||||
[&filename]( const LocatorProvider& provider ) {
|
||||
@@ -546,6 +548,7 @@ bool UniversalLocator::isCommand( const std::string& filename ) {
|
||||
}
|
||||
|
||||
std::optional<UniversalLocator::LocatorProvider> UniversalLocator::getLocator( const String& txt ) {
|
||||
Lock lock( mLocatorProvidersMutex );
|
||||
for ( const auto& locator : mLocatorProviders )
|
||||
if ( locator.matches( txt ) )
|
||||
return locator;
|
||||
@@ -554,6 +557,7 @@ std::optional<UniversalLocator::LocatorProvider> UniversalLocator::getLocator( c
|
||||
|
||||
bool UniversalLocator::isLocator( const String& txt ) {
|
||||
if ( !txt.empty() ) {
|
||||
Lock lock( mLocatorProvidersMutex );
|
||||
for ( const auto& locator : mLocatorProviders )
|
||||
if ( locator.matches( txt ) )
|
||||
return true;
|
||||
@@ -564,8 +568,26 @@ bool UniversalLocator::isLocator( const String& txt ) {
|
||||
bool UniversalLocator::tryLocator( const String& txt ) {
|
||||
if ( txt.empty() )
|
||||
return false;
|
||||
for ( const auto& locator : mLocatorProviders ) {
|
||||
if ( locator.matches( txt ) && locator.switchFn( txt ) )
|
||||
auto locator = getLocator( txt );
|
||||
if ( locator ) {
|
||||
if ( locator->modelFn ) {
|
||||
String query( txt.substr( locator->triggerSize( txt ) ) );
|
||||
query.trim();
|
||||
const Uint64 generation = ++mLocatorModelGeneration;
|
||||
locator->modelFn( query, [this, generation]( std::shared_ptr<Model> model ) {
|
||||
mUISceneNode->runOnMainThread( [this, generation, model = std::move( model )] {
|
||||
if ( generation != mLocatorModelGeneration || !mLocateBarLayout->isVisible() )
|
||||
return;
|
||||
mLocateTable->setModel( model );
|
||||
if ( model && model->hasChildren() )
|
||||
mLocateTable->getSelection().set( model->index( 0 ) );
|
||||
mLocateTable->scrollToTop();
|
||||
updateLocateBarSync();
|
||||
} );
|
||||
} );
|
||||
return true;
|
||||
}
|
||||
if ( locator->switchFn && locator->switchFn( txt ) )
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -573,23 +595,49 @@ bool UniversalLocator::tryLocator( const String& txt ) {
|
||||
|
||||
bool UniversalLocator::openLocator( const String& txt, const Variant& vName,
|
||||
const ModelEvent* modelEvent ) {
|
||||
for ( const auto& locator : mLocatorProviders ) {
|
||||
if ( locator.matches( txt ) && locator.openFn ) {
|
||||
locator.openFn( vName, modelEvent );
|
||||
return true;
|
||||
}
|
||||
auto locator = getLocator( txt );
|
||||
if ( locator && locator->openFn ) {
|
||||
locator->openFn( vName, modelEvent );
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UniversalLocator::pressEnterLocator( const String& txt ) {
|
||||
for ( const auto& locator : mLocatorProviders ) {
|
||||
if ( locator.matches( txt ) && locator.pressEnterFn && locator.pressEnterFn( txt ) )
|
||||
return true;
|
||||
}
|
||||
auto locator = getLocator( txt );
|
||||
if ( locator && locator->pressEnterFn && locator->pressEnterFn( txt ) )
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint64 UniversalLocator::registerLocatorProvider( LocatorProvider provider ) {
|
||||
if ( provider.symbol.empty() || ( !provider.switchFn && !provider.modelFn ) )
|
||||
return 0;
|
||||
Lock lock( mLocatorProvidersMutex );
|
||||
if ( std::find_if( mLocatorProviders.begin(), mLocatorProviders.end(),
|
||||
[&provider]( const LocatorProvider& current ) {
|
||||
return current.symbol == provider.symbol;
|
||||
} ) != mLocatorProviders.end() )
|
||||
return 0;
|
||||
provider.id = ++mLastLocatorProviderId;
|
||||
mLocatorProviders.emplace_back( std::move( provider ) );
|
||||
return mLastLocatorProviderId;
|
||||
}
|
||||
|
||||
bool UniversalLocator::unregisterLocatorProvider( Uint64 providerId ) {
|
||||
if ( providerId == 0 )
|
||||
return false;
|
||||
Lock lock( mLocatorProvidersMutex );
|
||||
auto provider = std::find_if(
|
||||
mLocatorProviders.begin(), mLocatorProviders.end(),
|
||||
[providerId]( const LocatorProvider& current ) { return current.id == providerId; } );
|
||||
if ( provider == mLocatorProviders.end() )
|
||||
return false;
|
||||
mLocatorProviders.erase( provider );
|
||||
++mLocatorModelGeneration;
|
||||
return true;
|
||||
}
|
||||
|
||||
void UniversalLocator::initLocateBar( UILocateBar* locateBar, UITextInput* locateInput ) {
|
||||
mLocateBarLayout = locateBar;
|
||||
mLocateInput = locateInput;
|
||||
@@ -1287,6 +1335,7 @@ std::vector<ProjectDirectoryTree::CommandInfo> UniversalLocator::getLocatorComma
|
||||
std::vector<ProjectDirectoryTree::CommandInfo> vec;
|
||||
UIIcon* icon = mUISceneNode->findIcon( "chevron-right" );
|
||||
bool isOpenFolder = !mApp->getCurrentProject().empty();
|
||||
Lock lock( mLocatorProvidersMutex );
|
||||
for ( const auto& locator : mLocatorProviders ) {
|
||||
if ( !isOpenFolder && locator.projectNeeded )
|
||||
continue;
|
||||
|
||||
@@ -18,11 +18,14 @@ class OpenDocumentsModel;
|
||||
class UniversalLocator {
|
||||
public:
|
||||
struct LocatorProvider {
|
||||
using ModelReadyFn = std::function<void( std::shared_ptr<Model> )>;
|
||||
using ModelFn = std::function<void( const String&, ModelReadyFn )>;
|
||||
|
||||
LocatorProvider(
|
||||
String&& symbol, String&& description, std::function<bool( const String& )> switchFn,
|
||||
std::function<void( const Variant& var, const ModelEvent* modelEvent )> openFn,
|
||||
std::function<bool( const String& )> pressEnterFn = nullptr, bool projectNeeded = true,
|
||||
bool spaceOptional = false ) :
|
||||
bool spaceOptional = false, ModelFn modelFn = nullptr ) :
|
||||
symbol( std::move( symbol ) ),
|
||||
symbolTrigger( this->symbol + " " ),
|
||||
description( std::move( description ) ),
|
||||
@@ -30,7 +33,8 @@ class UniversalLocator {
|
||||
openFn( std::move( openFn ) ),
|
||||
pressEnterFn( std::move( pressEnterFn ) ),
|
||||
projectNeeded( projectNeeded ),
|
||||
spaceOptional( spaceOptional ) {}
|
||||
spaceOptional( spaceOptional ),
|
||||
modelFn( std::move( modelFn ) ) {}
|
||||
|
||||
bool matches( const String& txt ) const {
|
||||
return String::startsWith( txt, symbolTrigger ) ||
|
||||
@@ -49,6 +53,8 @@ class UniversalLocator {
|
||||
std::function<bool( const String& )> pressEnterFn{ nullptr };
|
||||
bool projectNeeded{ true };
|
||||
bool spaceOptional{ false };
|
||||
ModelFn modelFn;
|
||||
Uint64 id{ 0 };
|
||||
};
|
||||
|
||||
UniversalLocator( UICodeEditorSplitter* editorSplitter, UISceneNode* sceneNode, App* app );
|
||||
@@ -91,6 +97,13 @@ class UniversalLocator {
|
||||
|
||||
void showCalculator();
|
||||
|
||||
/** Registers a locator provider and returns its opaque ownership handle, or zero when the
|
||||
* provider is invalid or its symbol is already registered. */
|
||||
Uint64 registerLocatorProvider( LocatorProvider provider );
|
||||
|
||||
/** Unregisters the locator provider associated with @p providerId. */
|
||||
bool unregisterLocatorProvider( Uint64 providerId );
|
||||
|
||||
protected:
|
||||
UILocateBar* mLocateBarLayout{ nullptr };
|
||||
UITableView* mLocateTable{ nullptr };
|
||||
@@ -107,6 +120,9 @@ class UniversalLocator {
|
||||
std::shared_ptr<OpenDocumentsModel> mOpenDocumentsModel{ nullptr };
|
||||
PluginIDType mQueryWorkspaceLastId;
|
||||
std::vector<LocatorProvider> mLocatorProviders;
|
||||
mutable Mutex mLocatorProvidersMutex;
|
||||
Uint64 mLastLocatorProviderId{ 0 };
|
||||
Uint64 mLocatorModelGeneration{ 0 };
|
||||
std::unordered_map<std::string, double> mCalculatorVariables;
|
||||
std::vector<std::tuple<std::string, std::string, std::string>> mCalculatorHistory;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user