- 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:
Martín Lucas Golini
2026-07-29 18:49:01 -03:00
parent 683b35b249
commit 1dc8195187
11 changed files with 660 additions and 182 deletions
+17 -2
View File
@@ -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;
+60 -11
View File
@@ -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 -2
View File
@@ -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;