Improve isJavaScriptRegEx in ParserMatcher.

Handle Recent Files/Recent Folders that have been removed from disk (SpartanJ/ecode#606).
Added a few new Claude models.
This commit is contained in:
Martín Lucas Golini
2025-08-07 00:56:44 -03:00
parent e5bd1dc3e1
commit 05c0f93aea
3 changed files with 193 additions and 43 deletions

View File

@@ -833,16 +833,61 @@ inline bool isValidRegexFlag( char c ) {
inline bool isRegexContextBefore( const char* str, int offset ) {
// Skip whitespace backwards
while ( offset > 0 && std::isspace( static_cast<unsigned char>( str[offset - 1] ) ) ) {
offset--;
int pos = offset;
while ( pos > 0 && std::isspace( static_cast<unsigned char>( str[pos - 1] ) ) ) {
pos--;
}
if ( offset == 0 )
return true;
if ( pos == 0 ) {
return true; // Beginning of string/line
}
// Common cases: after `return`, `(`, `=`, `,`, `:`
char prev = str[offset - 1];
return prev == '=' || prev == '(' || prev == '{' || prev == ',' || prev == ':' || prev == '[';
// Check for single character operators that can precede regex
char prev = str[pos - 1];
if ( prev == '=' || prev == '(' || prev == '{' || prev == ',' || prev == ':' || prev == '[' ||
prev == ';' || prev == '!' || prev == '?' || prev == '+' || prev == '-' || prev == '*' ||
prev == '/' || prev == '%' || prev == '<' || prev == '>' || prev == '^' || prev == '|' ||
prev == '&' || prev == '~' ) {
return true;
}
// Check for multi-character operators
if ( pos >= 2 ) {
char prev2 = str[pos - 2];
// Two-character operators: ==, !=, <=, >=, &&, ||, etc.
if ( ( prev == '=' && ( prev2 == '=' || prev2 == '!' || prev2 == '<' || prev2 == '>' ) ) ||
( prev == '&' && prev2 == '&' ) || ( prev == '|' && prev2 == '|' ) ||
( prev == '+' && prev2 == '+' ) || ( prev == '-' && prev2 == '-' ) ) {
return true;
}
// Three-character operators: ===, !==
if ( pos >= 3 && prev == '=' && prev2 == '=' &&
( str[pos - 3] == '=' || str[pos - 3] == '!' ) ) {
return true;
}
}
// Check for keywords that can precede regex
// We'll look backwards for common keywords
const char* keywords[] = { "return", "throw", "case", "in", "of", "typeof", "instanceof",
"new", "delete", "void", "if", "while", "for", "with" };
for ( const char* keyword : keywords ) {
size_t keywordLen = strlen( keyword );
if ( pos >= static_cast<int>( keywordLen ) ) {
// Check if we have the keyword followed by whitespace/end
if ( strncmp( &str[pos - keywordLen], keyword, keywordLen ) == 0 ) {
// Make sure it's a complete word (not part of identifier)
if ( pos == static_cast<int>( keywordLen ) ||
!std::isalnum( static_cast<unsigned char>( str[pos - keywordLen - 1] ) ) ) {
return true;
}
}
}
}
return false;
}
/**
@@ -857,17 +902,16 @@ inline bool isRegexContextBefore( const char* str, int offset ) {
*/
inline size_t isJavaScriptRegEx( const char* stringSearch, int stringStartOffset,
PatternMatcher::Range* matchList, size_t stringLength ) {
if ( !stringSearch || stringStartOffset < 0 || (size_t)stringStartOffset >= stringLength ) {
if ( !stringSearch || stringStartOffset < 0 ||
static_cast<size_t>( stringStartOffset ) >= stringLength ) {
return 0;
}
const char* ptr = stringSearch + stringStartOffset;
if ( *ptr != '/' ) {
if ( stringSearch[stringStartOffset] != '/' ) {
return 0;
}
// Heuristic check: likely a regex if it's in a valid context
// Enhanced context check
if ( !isRegexContextBefore( stringSearch, stringStartOffset ) ) {
return 0;
}
@@ -875,40 +919,81 @@ inline size_t isJavaScriptRegEx( const char* stringSearch, int stringStartOffset
int i = stringStartOffset + 1;
bool escaped = false;
bool insideCharClass = false;
bool foundContent = false; // Track if we found actual regex content
// Search for closing '/'
while ( (size_t)i < stringLength ) {
// Parse the regex body
while ( static_cast<size_t>( i ) < stringLength ) {
char c = stringSearch[i];
if ( !escaped && c == '\\' ) {
escaped = true;
foundContent = true;
i++;
continue;
}
if ( !escaped && c == '[' ) {
insideCharClass = true;
} else if ( !escaped && c == ']' ) {
insideCharClass = false;
} else if ( !escaped && c == '/' && !insideCharClass ) {
// Check for flags
i++;
while ( (size_t)i < stringLength && isValidRegexFlag( stringSearch[i] ) ) {
i++;
}
if ( !escaped ) {
if ( c == '[' ) {
insideCharClass = true;
foundContent = true;
} else if ( c == ']' && insideCharClass ) {
insideCharClass = false;
} else if ( c == '/' && !insideCharClass ) {
// Found closing slash
i++; // Move past the '/'
if ( matchList ) {
matchList[0].start = stringStartOffset;
matchList[0].end = i;
}
// Collect flags
int flagStart = i;
while ( static_cast<size_t>( i ) < stringLength &&
isValidRegexFlag( stringSearch[i] ) ) {
i++;
}
return 1;
// Additional validation: check for invalid patterns
// Empty regex // is usually a comment, not regex
if ( !foundContent && i == flagStart ) {
return 0; // Likely a comment start
}
// Check what comes after - should be end of statement or operator
if ( static_cast<size_t>( i ) < stringLength ) {
char nextChar = stringSearch[i];
// Skip whitespace
int nextPos = i;
while ( static_cast<size_t>( nextPos ) < stringLength &&
std::isspace( static_cast<unsigned char>( stringSearch[nextPos] ) ) ) {
nextPos++;
}
if ( static_cast<size_t>( nextPos ) < stringLength ) {
nextChar = stringSearch[nextPos];
// Should be followed by operators, semicolon, parentheses, etc.
// Not by alphanumeric characters (which would suggest division)
if ( std::isalnum( static_cast<unsigned char>( nextChar ) ) &&
nextChar != '(' && nextChar != '[' ) {
return 0; // Likely division, not regex
}
}
}
if ( matchList ) {
matchList[0].start = stringStartOffset;
matchList[0].end = i;
}
return 1;
} else if ( c == '\n' || c == '\r' ) {
// Unescaped newline in regex is invalid
return 0;
} else if ( c != ' ' && c != '\t' ) {
foundContent = true;
}
}
escaped = false;
i++;
}
// Reached end without finding closing '/'
return 0;
}

View File

@@ -961,8 +961,11 @@ void App::updateRecentFiles() {
->setId( "reopen-closed-tab" )
->setEnabled( !mRecentClosedFiles.empty() );
menu->addSeparator();
for ( const auto& file : mRecentFiles )
menu->add( file );
for ( const auto& file : mRecentFiles ) {
if ( ( FileSystem::fileExists( file ) && !FileSystem::isDirectory( file ) ) ||
String::startsWith( file, "https://" ) || String::startsWith( file, "http://" ) )
menu->add( file );
}
menu->addSeparator();
menu->add( i18n( "clear_menu", "Clear Menu" ) )->setId( "clear-menu" );
menu->on( Event::OnItemClicked, [this]( const Event* event ) {
@@ -982,6 +985,19 @@ void App::updateRecentFiles() {
String::startsWith( path, "https://" ) ||
String::startsWith( path, "http://" ) ) {
loadFileFromPathOrFocus( path );
} else {
auto msgBox = UIMessageBox::New(
UIMessageBox::YES_NO,
i18n( "file_does_not_exists_anymore_recreate",
"File does not exists anymore.\nDo you want to recreate it?" ) );
msgBox->setTitle( i18n( "file_not_found", "File not found" ) );
msgBox->on( Event::OnConfirm, [path, this]( auto ) {
FileSystem::fileWrite( path, "" );
loadFileFromPathOrFocus( path );
} );
msgBox->center();
msgBox->showWhenReady();
updateRecentFiles();
}
}
} );
@@ -1000,8 +1016,10 @@ void App::updateRecentFolders() {
menu->removeEventsOfType( Event::OnItemClicked );
if ( mRecentFolders.empty() )
return;
for ( const auto& file : mRecentFolders )
menu->add( file );
for ( const auto& file : mRecentFolders ) {
if ( FileSystem::fileExists( file ) && FileSystem::isDirectory( file ) )
menu->add( file );
}
menu->addSeparator();
menu->add( i18n( "clear_menu", "Clear Menu" ) )->setId( "clear-menu" );
menu->addCheckBox(
@@ -3238,6 +3256,17 @@ void App::saveSidePanelTabsOrder() {
}
void App::loadFolder( std::string path ) {
if ( !FileSystem::fileExists( path ) || !FileSystem::isDirectory( path ) ) {
auto msgBox = UIMessageBox::New(
UIMessageBox::OK, i18n( "directory_does_not_exist",
"The directory does not exists and cannot be opened" ) );
msgBox->setTitle( i18n( "invalid_directory", "Invalid Directory" ) );
msgBox->center();
msgBox->showWhenReady();
updateRecentFolders();
return;
}
Clock dirTreeClock;
if ( FileSystem::fileExtension( path ) == "lnk" ) {