From 05c0f93aea4b59d7b32b613a4fcf5aae5ac41cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 7 Aug 2025 00:56:44 -0300 Subject: [PATCH] Improve isJavaScriptRegEx in ParserMatcher. Handle Recent Files/Recent Folders that have been removed from disk (SpartanJ/ecode#606). Added a few new Claude models. --- bin/assets/plugins/aiassistant.json | 56 +++++++++-- src/eepp/system/parsermatcher.cpp | 143 ++++++++++++++++++++++------ src/tools/ecode/ecode.cpp | 37 ++++++- 3 files changed, 193 insertions(+), 43 deletions(-) diff --git a/bin/assets/plugins/aiassistant.json b/bin/assets/plugins/aiassistant.json index bfad2149a..3fb4458a7 100644 --- a/bin/assets/plugins/aiassistant.json +++ b/bin/assets/plugins/aiassistant.json @@ -11,10 +11,10 @@ "should_speculate": true }, "default_temperature": 1.0, - "display_name": "Claude 3.5 Sonnet", + "display_name": "Claude Opus 4.1", "max_output_tokens": 8192, "max_tokens": 200000, - "name": "claude-3-5-sonnet-latest" + "name": "claude-opus-4-1-20250805" }, { "cache_configuration": { @@ -23,10 +23,10 @@ "should_speculate": true }, "default_temperature": 1.0, - "display_name": "Claude 3.7 Sonnet", + "display_name": "Claude Opus 4", "max_output_tokens": 8192, "max_tokens": 200000, - "name": "claude-3-7-sonnet-latest" + "name": "claude-opus-4-20250514" }, { "cache_configuration": { @@ -35,24 +35,60 @@ "should_speculate": true }, "default_temperature": 1.0, - "display_name": "Claude 3.5 Haiku", + "display_name": "Claude Sonnet 4", "max_output_tokens": 8192, "max_tokens": 200000, - "name": "claude-3-5-haiku-latest", + "name": "claude-sonnet-4-20250514" + }, + { + "cache_configuration": { + "max_cache_anchors": 4, + "min_total_token": 2048, + "should_speculate": true + }, + "default_temperature": 1.0, + "display_name": "Claude Sonnet 3.5", + "max_output_tokens": 8192, + "max_tokens": 200000, + "name": "claude-3-5-sonnet-20241022" + }, + { + "cache_configuration": { + "max_cache_anchors": 4, + "min_total_token": 2048, + "should_speculate": true + }, + "default_temperature": 1.0, + "display_name": "Claude Sonnet 3.7", + "max_output_tokens": 8192, + "max_tokens": 200000, + "name": "claude-3-7-sonnet-20250219" + }, + { + "cache_configuration": { + "max_cache_anchors": 4, + "min_total_token": 2048, + "should_speculate": true + }, + "default_temperature": 1.0, + "display_name": "Claude Haiku 3.5", + "max_output_tokens": 8192, + "max_tokens": 200000, + "name": "claude-3-5-haiku-20241022", "cheapest": true }, { "cache_configuration": null, "default_temperature": 1.0, - "display_name": "Claude 3 Opus", + "display_name": "Claude Opus 3", "max_output_tokens": 4096, "max_tokens": 200000, - "name": "claude-3-opus-latest" + "name": "claude-3-opus-20240229" }, { "cache_configuration": null, "default_temperature": 1.0, - "display_name": "Claude 3 Sonnet", + "display_name": "Claude Sonnet 3", "max_output_tokens": 4096, "max_tokens": 200000, "name": "claude-3-sonnet-20240229" @@ -64,7 +100,7 @@ "should_speculate": true }, "default_temperature": 1.0, - "display_name": "Claude 3 Haiku", + "display_name": "Claude Haiku 3", "max_output_tokens": 4096, "max_tokens": 200000, "name": "claude-3-haiku-20240307" diff --git a/src/eepp/system/parsermatcher.cpp b/src/eepp/system/parsermatcher.cpp index 0a8bdef92..dfeb90eaa 100644 --- a/src/eepp/system/parsermatcher.cpp +++ b/src/eepp/system/parsermatcher.cpp @@ -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( str[offset - 1] ) ) ) { - offset--; + int pos = offset; + while ( pos > 0 && std::isspace( static_cast( 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( 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( keywordLen ) || + !std::isalnum( static_cast( 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( 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( 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( 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( i ) < stringLength ) { + char nextChar = stringSearch[i]; + // Skip whitespace + int nextPos = i; + while ( static_cast( nextPos ) < stringLength && + std::isspace( static_cast( stringSearch[nextPos] ) ) ) { + nextPos++; + } + + if ( static_cast( 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( 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; } diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index d623c59ce..b6c49e049 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -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" ) {