- add reusable-buffer String::toUtf8() conversion
  - extract shared find-bar CSS into UIFindBarStyle
  - reuse the shared style from UIDocFindReplace

  ecode:
  - enable terminal find, next, and previous shortcuts
  - install terminal bindings after conflicting editor bindings are removed

  eterm:
  - add searchable terminal history with plain text, RegEx, and LuaPattern modes
  - support case-sensitive and whole-word matching across wrapped rows
  - debounce searches and avoid unnecessary string allocations
  - add an animated find bar with status and wrapped result navigation
  - expose find commands through configurable keybindings and the context menu
  - render and navigate search matches in terminal history
  - forward right-button releases when applications capture the mouse
  - fix UITabWidgetSplitter ownership during shutdown

  tests:
  - cover UTF-8 buffer reuse and terminal search behavior
  - verify wrapped lines, Unicode, pattern modes, invalid expressions, and navigation
  - verify SGR right-button release encoding

  tooling:
  - clarify the mandatory optimized build workflow for performance work
  - require direct release test execution before falling back to Xvfb
This commit is contained in:
Martín Lucas Golini
2026-09-10 00:57:56 -03:00
parent a294d78733
commit 95d52cdbb3
25 changed files with 1067 additions and 120 deletions
+7 -6
View File
@@ -2112,16 +2112,17 @@ std::wstring String::toWideString() const {
#endif
std::string String::toUtf8() const {
// Prepare the output string
std::string output;
output.reserve( mString.length() + 1 );
// Convert
Utf32::toUtf8( mString.begin(), mString.end(), std::back_inserter( output ) );
toUtf8( output );
return output;
}
void String::toUtf8( std::string& output ) const {
output.clear();
output.reserve( mString.length() + 1 );
Utf32::toUtf8( mString.begin(), mString.end(), std::back_inserter( output ) );
}
std::basic_string<char16_t> String::toUtf16() const {
// Prepare the output string
std::basic_string<char16_t> output;
+2 -106
View File
@@ -1,111 +1,13 @@
#include <eepp/scene/actions/actions.hpp>
#include <eepp/system/scopedop.hpp>
#include <eepp/ui/css/stylesheetparser.hpp>
#include <eepp/ui/tools/uidocfindreplace.hpp>
#include <eepp/ui/tools/uifindbarstyle.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/window/clipboard.hpp>
#include <eepp/window/window.hpp>
namespace EE { namespace UI { namespace Tools {
const char* DOC_FIND_REPLACE_CSS_MARKER = "ce_find_replace_box_marker";
const char DOC_FIND_REPLACE_CSS[] = R"css(
.ce_find_replace_box {
background-color: var(--list-back);
}
.ce_find_replace_box > .find_replace_toggle,
.ce_find_replace_box pushbutton {
background-color: var(--list-back);
border-width: 1dp;
border-radius: 4dp;
border-color: transparent;
tint: var(--font);
text-as-fallback: true;
}
.ce_find_replace_box pushbutton:hover {
background-color: var(--list-back);
border-color: var(--primary);
}
.ce_find_replace_box > .find_replace_toggle:hover {
background-color: var(--list-back);
tint: var(--primary);
}
.ce_find_replace_box > linearlayout {
padding-top: 2dp;
padding-bottom: 2dp;
padding-right: 2dp;
}
.ce_find_replace_box > .expander {
background-color: var(--back);
}
.ce_find_replace_box .replace_box {
visible: false;
margin-top: 2dp;
}
.ce_find_replace_box .replace_box {
visible: false;
}
.ce_find_replace_box .replace_box.enabled {
visible: true;
}
.ce_find_replace_box > .find_replace_toggle {
icon: icon(arrow-right-s, 16dp);
padding: 2dp;
}
.ce_find_replace_box > .find_replace_toggle.enabled {
icon: icon(arrow-down-s, 16dp);
}
.ce_find_replace_box .input-find {
padding-right: 88dp;
clip: padding-box;
}
.ce_find_replace_box selectbutton {
background-color: transparent;
border-color: transparent;
padding: 2dp;
select-on-click: true;
transition: tint 0.2s;
border-radius: 2dp;
}
.ce_find_replace_box selectbutton:hover {
border-color: transparent;
tint: var(--primary);
color: var(--primary);
}
.ce_find_replace_box selectbutton:selected {
border-color: var(--primary);
background-color: rgba(var(--primary), 0.25);
color: var(--font);
}
.ce_find_replace_box selectbutton.match-case {
icon: url("data:image/svg,<svg fill='#ffffff' width='16' height='16' viewBox='0 0 16 16'><path d='M8.85352 11.7021H7.85449L7.03809 9.54297H3.77246L3.00439 11.7021H2L4.9541 4H5.88867L8.85352 11.7021ZM6.74268 8.73193L5.53418 5.4502C5.49479 5.34277 5.4554 5.1709 5.41602 4.93457H5.39453C5.35872 5.15299 5.31755 5.32487 5.271 5.4502L4.07324 8.73193H6.74268Z'/><path d='M13.756 11.7021H12.8752V10.8428H12.8537C12.4706 11.5016 11.9066 11.8311 11.1618 11.8311C10.6139 11.8311 10.1843 11.686 9.87273 11.396C9.56479 11.106 9.41082 10.721 9.41082 10.2412C9.41082 9.21354 10.016 8.61556 11.2262 8.44727L12.8752 8.21631C12.8752 7.28174 12.4974 6.81445 11.7419 6.81445C11.0794 6.81445 10.4815 7.04004 9.94793 7.49121V6.58887C10.4886 6.24512 11.1117 6.07324 11.8171 6.07324C13.1097 6.07324 13.756 6.75716 13.756 8.125V11.7021ZM12.8752 8.91992L11.5485 9.10254C11.1403 9.15983 10.8324 9.26188 10.6247 9.40869C10.417 9.55192 10.3132 9.80794 10.3132 10.1768C10.3132 10.4453 10.4081 10.6655 10.5978 10.8374C10.7912 11.0057 11.0472 11.0898 11.3659 11.0898C11.8027 11.0898 12.1626 10.9377 12.4455 10.6333C12.7319 10.3254 12.8752 9.93685 12.8752 9.46777V8.91992Z'/></svg>");
}
.ce_find_replace_box selectbutton.luapattern {
icon: url("data:image/svg,<svg width='16' height='16' fill='none'><path d='m 4.8848115,11.097205 a 3.7631475,3.7631475 0 0 0 5.3533025,0 3.840684,3.840684 0 0 0 0,-5.3992983 3.7631475,3.7631475 0 0 0 -5.3533025,0 3.840684,3.840684 0 0 0 0,5.3992983 z m -0.7300257,0.73594 c -1.8819023,-1.8970156 -1.8819023,-4.9741625 0,-6.8711778 a 4.7895202,4.7895202 0 0 1 6.8133542,0 c 1.881245,1.8976724 1.881245,4.9741622 0,6.8711778 a 4.7895202,4.7895202 0 0 1 -6.8140113,0 z M 12.954519,4.5204035 a 1.0263727,1.0263727 0 0 1 -1.460051,0 1.0473995,1.0473995 0 0 1 0,-1.4718788 1.0263727,1.0263727 0 0 1 1.460051,0 1.0473995,1.0473995 0 0 1 0,1.4718788 z' fill='#ffffff' style='stroke-width:0.657089' /><path d='m 10.525919,8.4419094 a 2.0527454,2.0527454 0 0 1 -2.9201028,0 2.0947991,2.0947991 0 0 1 0,-2.9444148 2.0527454,2.0527454 0 0 1 2.9201028,0 2.0947991,2.0947991 0 0 1 0,2.9444148 z M 8.3351847,7.70597 a 1.0263727,1.0263727 0 0 0 1.4600513,0 1.0473995,1.0473995 0 0 0 0,-1.4718789 1.0263727,1.0263727 0 0 0 -1.4600513,0 1.0473995,1.0473995 0 0 0 0,1.4718789 z' fill='#ffffff' style='stroke-width:0.657089' /></svg>");
}
.ce_find_replace_box selectbutton.regex {
icon: url("data:image/svg,<svg fill='#ffffff' width='16' height='16' viewBox='0 0 16 16'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.012 2h.976v3.113l2.56-1.557.486.885L11.47 6l2.564 1.559-.485.885-2.561-1.557V10h-.976V6.887l-2.56 1.557-.486-.885L9.53 6 6.966 4.441l.485-.885 2.561 1.557V2zM2 10h4v4H2v-4z'/></svg>");
}
.ce_find_replace_box selectbutton.whole-word {
icon: url("data:image/svg,<svg fill='#ffffff' width='16' height='16' viewBox='0 0 16 16'><path fill-rule='evenodd' clip-rule='evenodd' d='M0 11H1V13H15V11H16V14H15H1H0V11Z'/><path d='M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z'/><path d='M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z'/></svg>");
}
.ce_find_replace_box selectbutton.escape-sequences {
font-style: bold;
min-width: 20dp;
}
.ce_find_replace_box pushbutton.replace-button {
icon: url("data:image/svg,<svg width='16' height='16' viewBox='0 0 16 16' fill='#ffffff'><path fill-rule='evenodd' clip-rule='evenodd' d='M3.221 3.739l2.261 2.269L7.7 3.784l-.7-.7-1.012 1.007-.008-1.6a.523.523 0 0 1 .5-.526H8V1H6.48A1.482 1.482 0 0 0 5 2.489V4.1L3.927 3.033l-.706.706zm6.67 1.794h.01c.183.311.451.467.806.467.393 0 .706-.168.94-.503.236-.335.353-.78.353-1.333 0-.511-.1-.913-.301-1.207-.201-.295-.488-.442-.86-.442-.405 0-.718.194-.938.581h-.01V1H9v4.919h.89v-.386zm-.015-1.061v-.34c0-.248.058-.448.175-.601a.54.54 0 0 1 .445-.23.49.49 0 0 1 .436.233c.104.154.155.368.155.643 0 .33-.056.587-.169.768a.524.524 0 0 1-.47.27.495.495 0 0 1-.411-.211.853.853 0 0 1-.16-.532zM9 12.769c-.256.154-.625.231-1.108.231-.563 0-1.02-.178-1.369-.533-.349-.355-.523-.813-.523-1.374 0-.648.186-1.158.56-1.53.374-.376.875-.563 1.5-.563.433 0 .746.06.94.179v.998a1.26 1.26 0 0 0-.792-.276c-.325 0-.583.1-.774.298-.19.196-.283.468-.283.816 0 .338.09.603.272.797.182.191.431.287.749.287.282 0 .558-.092.828-.276v.946zM4 7L3 8v6l1 1h7l1-1V8l-1-1H4zm0 1h7v6H4V8z'/></svg>");
}
.ce_find_replace_box pushbutton.replace-all-button {
icon: url("data:image/svg,<svg width='16' height='16' viewBox='0 0 16 16' fill='#ffffff'><path fill-rule='evenodd' clip-rule='evenodd' d='M11.6 2.677c.147-.31.356-.465.626-.465.248 0 .44.118.573.353.134.236.201.557.201.966 0 .443-.078.798-.235 1.067-.156.268-.365.402-.627.402-.237 0-.416-.125-.537-.374h-.008v.31H11V1h.593v1.677h.008zm-.016 1.1a.78.78 0 0 0 .107.426c.071.113.163.169.274.169.136 0 .24-.072.314-.216.075-.145.113-.35.113-.615 0-.22-.035-.39-.104-.514-.067-.124-.164-.187-.29-.187-.12 0-.219.062-.297.185a.886.886 0 0 0-.117.48v.272zM4.12 7.695L2 5.568l.662-.662 1.006 1v-1.51A1.39 1.39 0 0 1 5.055 3H7.4v.905H5.055a.49.49 0 0 0-.468.493l.007 1.5.949-.944.656.656-2.08 2.085zM9.356 4.93H10V3.22C10 2.408 9.685 2 9.056 2c-.135 0-.285.024-.45.073a1.444 1.444 0 0 0-.388.167v.665c.237-.203.487-.304.75-.304.261 0 .392.156.392.469l-.6.103c-.506.086-.76.406-.76.961 0 .263.061.473.183.631A.61.61 0 0 0 8.69 5c.29 0 .509-.16.657-.48h.009v.41zm.004-1.355v.193a.75.75 0 0 1-.12.436.368.368 0 0 1-.313.17.276.276 0 0 1-.22-.095.38.38 0 0 1-.08-.248c0-.222.11-.351.332-.389l.4-.067zM7 12.93h-.644v-.41h-.009c-.148.32-.367.48-.657.48a.61.61 0 0 1-.507-.235c-.122-.158-.183-.368-.183-.63 0-.556.254-.876.76-.962l.6-.103c0-.313-.13-.47-.392-.47-.263 0-.513.102-.75.305v-.665c.095-.063.224-.119.388-.167.165-.049.315-.073.45-.073.63 0 .944.407.944 1.22v1.71zm-.64-1.162v-.193l-.4.068c-.222.037-.333.166-.333.388 0 .1.027.183.08.248a.276.276 0 0 0 .22.095.368.368 0 0 0 .312-.17c.08-.116.12-.26.12-.436zM9.262 13c.321 0 .568-.058.738-.173v-.71a.9.9 0 0 1-.552.207.619.619 0 0 1-.5-.215c-.12-.145-.181-.345-.181-.598 0-.26.063-.464.189-.612a.644.644 0 0 1 .516-.223c.194 0 .37.069.528.207v-.749c-.129-.09-.338-.134-.626-.134-.417 0-.751.14-1.001.422-.249.28-.373.662-.373 1.148 0 .42.116.764.349 1.03.232.267.537.4.913.4zM2 9l1-1h9l1 1v5l-1 1H3l-1-1V9zm1 0v5h9V9H3zm3-2l1-1h7l1 1v5l-1 1V7H6z'/></svg>");
}
.ce_find_replace_box .input-find.error,
.ce_find_replace_box .input-replace.error {
border-color: var(--theme-error);
}
)css";
const char DOC_FIND_REPLACE_XML[] = R"xml(
<hbox class="ce_find_replace_box" layout_width="wrap_content" layout_height="wrap_content" layout_gravity="right|top" margin_right="32dp">
<Widget class="expander" layout_width="2dp" layout_height="match_parent" />
@@ -173,13 +75,7 @@ UIDocFindReplace::UIDocFindReplace( UIWidget* parent, const std::shared_ptr<Doc:
getKeyBindings().addKeybindsStringUnordered( keybindings );
if ( !parent->getUISceneNode()->getStyleSheet().markerExists(
String::hash( DOC_FIND_REPLACE_CSS_MARKER ) ) ) {
CSS::StyleSheetParser parser;
parser.loadFromMemory( (const Uint8*)DOC_FIND_REPLACE_CSS,
eeARRAY_SIZE( DOC_FIND_REPLACE_CSS ) );
parent->getUISceneNode()->getStyleSheet().combineStyleSheet( parser.getStyleSheet() );
}
UIFindBarStyle::ensure( parent->getUISceneNode() );
parent->getUISceneNode()->loadLayoutFromMemory( DOC_FIND_REPLACE_XML,
eeARRAY_SIZE( DOC_FIND_REPLACE_XML ), this );
+119
View File
@@ -0,0 +1,119 @@
#include <eepp/ui/css/stylesheetparser.hpp>
#include <eepp/ui/tools/uifindbarstyle.hpp>
#include <eepp/ui/uiscenenode.hpp>
namespace EE { namespace UI { namespace Tools {
static constexpr auto FIND_BAR_STYLE_MARKER = String::hash( "ce_find_replace_box_marker" );
const char DOC_FIND_REPLACE_CSS[] = R"css(
.ce_find_replace_box {
background-color: var(--list-back);
}
.ce_find_replace_box > .find_replace_toggle,
.ce_find_replace_box pushbutton {
background-color: var(--list-back);
border-width: 1dp;
border-radius: 4dp;
border-color: transparent;
tint: var(--font);
text-as-fallback: true;
}
.ce_find_replace_box pushbutton:hover {
background-color: var(--list-back);
border-color: var(--primary);
}
.ce_find_replace_box > .find_replace_toggle:hover {
background-color: var(--list-back);
tint: var(--primary);
}
.ce_find_replace_box > linearlayout {
padding-top: 2dp;
padding-bottom: 2dp;
padding-right: 2dp;
}
.ce_find_replace_box > .expander {
background-color: var(--back);
}
.ce_find_replace_box .replace_box {
visible: false;
margin-top: 2dp;
}
.ce_find_replace_box .replace_box {
visible: false;
}
.ce_find_replace_box .replace_box.enabled {
visible: true;
}
.ce_find_replace_box > .find_replace_toggle {
icon: icon(arrow-right-s, 16dp);
padding: 2dp;
}
.ce_find_replace_box > .find_replace_toggle.enabled {
icon: icon(arrow-down-s, 16dp);
}
.ce_find_replace_box .input-find {
padding-right: 88dp;
clip: padding-box;
}
.ce_find_replace_box selectbutton {
background-color: transparent;
border-color: transparent;
padding: 2dp;
select-on-click: true;
transition: tint 0.2s;
border-radius: 2dp;
}
.ce_find_replace_box selectbutton:hover {
border-color: transparent;
tint: var(--primary);
color: var(--primary);
}
.ce_find_replace_box selectbutton:selected {
border-color: var(--primary);
background-color: rgba(var(--primary), 0.25);
color: var(--font);
}
.ce_find_replace_box selectbutton.match-case {
icon: url("data:image/svg,<svg fill='#ffffff' width='16' height='16' viewBox='0 0 16 16'><path d='M8.85352 11.7021H7.85449L7.03809 9.54297H3.77246L3.00439 11.7021H2L4.9541 4H5.88867L8.85352 11.7021ZM6.74268 8.73193L5.53418 5.4502C5.49479 5.34277 5.4554 5.1709 5.41602 4.93457H5.39453C5.35872 5.15299 5.31755 5.32487 5.271 5.4502L4.07324 8.73193H6.74268Z'/><path d='M13.756 11.7021H12.8752V10.8428H12.8537C12.4706 11.5016 11.9066 11.8311 11.1618 11.8311C10.6139 11.8311 10.1843 11.686 9.87273 11.396C9.56479 11.106 9.41082 10.721 9.41082 10.2412C9.41082 9.21354 10.016 8.61556 11.2262 8.44727L12.8752 8.21631C12.8752 7.28174 12.4974 6.81445 11.7419 6.81445C11.0794 6.81445 10.4815 7.04004 9.94793 7.49121V6.58887C10.4886 6.24512 11.1117 6.07324 11.8171 6.07324C13.1097 6.07324 13.756 6.75716 13.756 8.125V11.7021ZM12.8752 8.91992L11.5485 9.10254C11.1403 9.15983 10.8324 9.26188 10.6247 9.40869C10.417 9.55192 10.3132 9.80794 10.3132 10.1768C10.3132 10.4453 10.4081 10.6655 10.5978 10.8374C10.7912 11.0057 11.0472 11.0898 11.3659 11.0898C11.8027 11.0898 12.1626 10.9377 12.4455 10.6333C12.7319 10.3254 12.8752 9.93685 12.8752 9.46777V8.91992Z'/></svg>");
}
.ce_find_replace_box selectbutton.luapattern {
icon: url("data:image/svg,<svg width='16' height='16' fill='none'><path d='m 4.8848115,11.097205 a 3.7631475,3.7631475 0 0 0 5.3533025,0 3.840684,3.840684 0 0 0 0,-5.3992983 3.7631475,3.7631475 0 0 0 -5.3533025,0 3.840684,3.840684 0 0 0 0,5.3992983 z m -0.7300257,0.73594 c -1.8819023,-1.8970156 -1.8819023,-4.9741625 0,-6.8711778 a 4.7895202,4.7895202 0 0 1 6.8133542,0 c 1.881245,1.8976724 1.881245,4.9741622 0,6.8711778 a 4.7895202,4.7895202 0 0 1 -6.8140113,0 z M 12.954519,4.5204035 a 1.0263727,1.0263727 0 0 1 -1.460051,0 1.0473995,1.0473995 0 0 1 0,-1.4718788 1.0263727,1.0263727 0 0 1 1.460051,0 1.0473995,1.0473995 0 0 1 0,1.4718788 z' fill='#ffffff' style='stroke-width:0.657089' /><path d='m 10.525919,8.4419094 a 2.0527454,2.0527454 0 0 1 -2.9201028,0 2.0947991,2.0947991 0 0 1 0,-2.9444148 2.0527454,2.0527454 0 0 1 2.9201028,0 2.0947991,2.0947991 0 0 1 0,2.9444148 z M 8.3351847,7.70597 a 1.0263727,1.0263727 0 0 0 1.4600513,0 1.0473995,1.0473995 0 0 0 0,-1.4718789 1.0263727,1.0263727 0 0 0 -1.4600513,0 1.0473995,1.0473995 0 0 0 0,1.4718789 z' fill='#ffffff' style='stroke-width:0.657089' /></svg>");
}
.ce_find_replace_box selectbutton.regex {
icon: url("data:image/svg,<svg fill='#ffffff' width='16' height='16' viewBox='0 0 16 16'><path fill-rule='evenodd' clip-rule='evenodd' d='M10.012 2h.976v3.113l2.56-1.557.486.885L11.47 6l2.564 1.559-.485.885-2.561-1.557V10h-.976V6.887l-2.56 1.557-.486-.885L9.53 6 6.966 4.441l.485-.885 2.561 1.557V2zM2 10h4v4H2v-4z'/></svg>");
}
.ce_find_replace_box selectbutton.whole-word {
icon: url("data:image/svg,<svg fill='#ffffff' width='16' height='16' viewBox='0 0 16 16'><path fill-rule='evenodd' clip-rule='evenodd' d='M0 11H1V13H15V11H16V14H15H1H0V11Z'/><path d='M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z'/><path d='M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z'/></svg>");
}
.ce_find_replace_box selectbutton.escape-sequences {
font-style: bold;
min-width: 20dp;
}
.ce_find_replace_box pushbutton.replace-button {
icon: url("data:image/svg,<svg width='16' height='16' viewBox='0 0 16 16' fill='#ffffff'><path fill-rule='evenodd' clip-rule='evenodd' d='M3.221 3.739l2.261 2.269L7.7 3.784l-.7-.7-1.012 1.007-.008-1.6a.523.523 0 0 1 .5-.526H8V1H6.48A1.482 1.482 0 0 0 5 2.489V4.1L3.927 3.033l-.706.706zm6.67 1.794h.01c.183.311.451.467.806.467.393 0 .706-.168.94-.503.236-.335.353-.78.353-1.333 0-.511-.1-.913-.301-1.207-.201-.295-.488-.442-.86-.442-.405 0-.718.194-.938.581h-.01V1H9v4.919h.89v-.386zm-.015-1.061v-.34c0-.248.058-.448.175-.601a.54.54 0 0 1 .445-.23.49.49 0 0 1 .436.233c.104.154.155.368.155.643 0 .33-.056.587-.169.768a.524.524 0 0 1-.47.27.495.495 0 0 1-.411-.211.853.853 0 0 1-.16-.532zM9 12.769c-.256.154-.625.231-1.108.231-.563 0-1.02-.178-1.369-.533-.349-.355-.523-.813-.523-1.374 0-.648.186-1.158.56-1.53.374-.376.875-.563 1.5-.563.433 0 .746.06.94.179v.998a1.26 1.26 0 0 0-.792-.276c-.325 0-.583.1-.774.298-.19.196-.283.468-.283.816 0 .338.09.603.272.797.182.191.431.287.749.287.282 0 .558-.092.828-.276v.946zM4 7L3 8v6l1 1h7l1-1V8l-1-1H4zm0 1h7v6H4V8z'/></svg>");
}
.ce_find_replace_box pushbutton.replace-all-button {
icon: url("data:image/svg,<svg width='16' height='16' viewBox='0 0 16 16' fill='#ffffff'><path fill-rule='evenodd' clip-rule='evenodd' d='M11.6 2.677c.147-.31.356-.465.626-.465.248 0 .44.118.573.353.134.236.201.557.201.966 0 .443-.078.798-.235 1.067-.156.268-.365.402-.627.402-.237 0-.416-.125-.537-.374h-.008v.31H11V1h.593v1.677h.008zm-.016 1.1a.78.78 0 0 0 .107.426c.071.113.163.169.274.169.136 0 .24-.072.314-.216.075-.145.113-.35.113-.615 0-.22-.035-.39-.104-.514-.067-.124-.164-.187-.29-.187-.12 0-.219.062-.297.185a.886.886 0 0 0-.117.48v.272zM4.12 7.695L2 5.568l.662-.662 1.006 1v-1.51A1.39 1.39 0 0 1 5.055 3H7.4v.905H5.055a.49.49 0 0 0-.468.493l.007 1.5.949-.944.656.656-2.08 2.085zM9.356 4.93H10V3.22C10 2.408 9.685 2 9.056 2c-.135 0-.285.024-.45.073a1.444 1.444 0 0 0-.388.167v.665c.237-.203.487-.304.75-.304.261 0 .392.156.392.469l-.6.103c-.506.086-.76.406-.76.961 0 .263.061.473.183.631A.61.61 0 0 0 8.69 5c.29 0 .509-.16.657-.48h.009v.41zm.004-1.355v.193a.75.75 0 0 1-.12.436.368.368 0 0 1-.313.17.276.276 0 0 1-.22-.095.38.38 0 0 1-.08-.248c0-.222.11-.351.332-.389l.4-.067zM7 12.93h-.644v-.41h-.009c-.148.32-.367.48-.657.48a.61.61 0 0 1-.507-.235c-.122-.158-.183-.368-.183-.63 0-.556.254-.876.76-.962l.6-.103c0-.313-.13-.47-.392-.47-.263 0-.513.102-.75.305v-.665c.095-.063.224-.119.388-.167.165-.049.315-.073.45-.073.63 0 .944.407.944 1.22v1.71zm-.64-1.162v-.193l-.4.068c-.222.037-.333.166-.333.388 0 .1.027.183.08.248a.276.276 0 0 0 .22.095.368.368 0 0 0 .312-.17c.08-.116.12-.26.12-.436zM9.262 13c.321 0 .568-.058.738-.173v-.71a.9.9 0 0 1-.552.207.619.619 0 0 1-.5-.215c-.12-.145-.181-.345-.181-.598 0-.26.063-.464.189-.612a.644.644 0 0 1 .516-.223c.194 0 .37.069.528.207v-.749c-.129-.09-.338-.134-.626-.134-.417 0-.751.14-1.001.422-.249.28-.373.662-.373 1.148 0 .42.116.764.349 1.03.232.267.537.4.913.4zM2 9l1-1h9l1 1v5l-1 1H3l-1-1V9zm1 0v5h9V9H3zm3-2l1-1h7l1 1v5l-1 1V7H6z'/></svg>");
}
.ce_find_replace_box .input-find.error,
.ce_find_replace_box .input-replace.error {
border-color: var(--theme-error);
}
)css";
const char* UIFindBarStyle::getStyleSheet() {
return DOC_FIND_REPLACE_CSS;
}
void UIFindBarStyle::ensure( UISceneNode* scene ) {
if ( scene->getStyleSheet().markerExists( FIND_BAR_STYLE_MARKER ) )
return;
CSS::StyleSheetParser parser;
parser.loadFromMemory( reinterpret_cast<const Uint8*>( DOC_FIND_REPLACE_CSS ),
sizeof( DOC_FIND_REPLACE_CSS ) );
parser.getStyleSheet().setMarker( FIND_BAR_STYLE_MARKER );
scene->getStyleSheet().combineStyleSheet( parser.getStyleSheet() );
}
}}} // namespace EE::UI::Tools
@@ -258,6 +258,18 @@ class TerminalDisplay {
const std::shared_ptr<TerminalSession>& getSession() const;
void setSearchQuery( TerminalSearchQuery query );
void navigateSearch( int direction );
void clearSearch();
Uint32 getSearchMatchCount() const;
Int32 getCurrentSearchMatch() const;
Uint64 getSearchRequestId() const;
std::string getSelection();
bool hasSelection() const;
@@ -43,6 +43,7 @@
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/kittygraphicsprotocol.hpp>
#include <eterm/terminal/kittykeyboardprotocol.hpp>
#include <eterm/terminal/terminalsearch.hpp>
#include <eterm/terminal/terminaltypes.hpp>
#include <memory>
#include <stdint.h>
@@ -297,6 +298,18 @@ class TerminalEmulator final {
int getTerminalMode() const { return mTerm.mode; }
void setSearchQuery( TerminalSearchQuery query );
void navigateSearch( int direction );
void clearSearch();
const std::vector<TerminalSearchMatch>& getSearchMatches() const;
Int32 getCurrentSearchMatch() const;
Uint64 getSearchRequestId() const { return mSearchQuery.requestId; }
private:
DpyPtr mDpy;
PtyPtr mPty;
@@ -361,6 +374,12 @@ class TerminalEmulator final {
};
std::unordered_map<const TerminalGlyph*, KittyPlaceholderMetadata> mKittyPlaceholderMetadata;
Uint32 mKittyUnderlineColor{ 0 };
TerminalSearch mSearch;
TerminalSearchQuery mSearchQuery;
std::vector<TerminalSearchRowView> mSearchRows;
Int32 mCurrentSearchMatch{ -1 };
Clock mSearchRefreshClock;
bool mSearchDirty{ false };
void setClipboard( const char* str );
@@ -0,0 +1,82 @@
#ifndef ETERM_TERMINALSEARCH_HPP
#define ETERM_TERMINALSEARCH_HPP
#include <eepp/core/string.hpp>
#include <eterm/terminal/terminaltypes.hpp>
#include <vector>
namespace EE { namespace System {
class PatternMatcher;
}} // namespace EE::System
using namespace EE;
using namespace EE::System;
namespace eterm { namespace Terminal {
enum class TerminalBufferSource : Uint8 { MainHistory, MainScreen, AlternateScreen };
enum class TerminalSearchType : Uint8 { Normal, RegEx, LuaPattern };
struct TerminalBufferPosition {
TerminalBufferSource source{ TerminalBufferSource::MainScreen };
Int64 row{ 0 };
Int32 column{ 0 };
};
struct TerminalSearchRowView {
Line cells{ nullptr };
TerminalBufferSource source{ TerminalBufferSource::MainScreen };
Int64 row{ 0 };
Int32 width{ 0 };
Int32 length{ 0 };
bool wrapped{ false };
};
struct TerminalSearchQuery {
String text;
Uint64 requestId{ 0 };
bool caseSensitive{ false };
bool wholeWord{ false };
TerminalSearchType type{ TerminalSearchType::Normal };
};
struct TerminalSearchMatch {
TerminalBufferPosition start;
TerminalBufferPosition end;
};
/** Worker-side search engine over chronological, non-owning terminal rows. */
class TerminalSearch {
public:
static constexpr size_t MinimumQueryLength = 2;
static bool isQuerySearchable( const TerminalSearchQuery& query ) {
return query.text.size() >= MinimumQueryLength;
}
const std::vector<TerminalSearchMatch>& search( const std::vector<TerminalSearchRowView>& rows,
const TerminalSearchQuery& query );
const std::vector<TerminalSearchMatch>& matches() const { return mMatches; }
private:
struct CellSpan {
TerminalBufferPosition start;
TerminalBufferPosition end;
};
void searchLogicalLine( const TerminalSearchQuery& query, PatternMatcher* pattern,
const String& needle );
std::vector<TerminalSearchMatch> mMatches;
String mText;
String mComparableText;
std::string mUtf8Text;
std::vector<CellSpan> mPositions;
};
}} // namespace eterm::Terminal
#endif
@@ -30,9 +30,16 @@ namespace eterm { namespace Terminal {
/** Immutable worker-to-UI presentation state. Cell selection is already applied as ATTR_REVERSE. */
struct TerminalSnapshot {
struct SearchCellRange {
Vector2i start;
Vector2i end;
bool active{ false };
};
std::shared_ptr<const TerminalGraphicsPresentation> graphics;
std::vector<TerminalGlyph> cells;
std::vector<Uint8> dirtyRows;
std::vector<SearchCellRange> visibleSearchMatches;
std::string title;
std::string currentWorkingDirectory;
std::string selection;
@@ -48,6 +55,9 @@ struct TerminalSnapshot {
int processId{ 0 };
int exitCode{ 0 };
Uint32 presentationRate{ 60 };
Uint32 searchMatchCount{ 0 };
Int32 currentSearchMatch{ -1 };
Uint64 searchRequestId{ 0 };
TerminalCursorMode cursorMode{ SteadyUnderline };
TerminalSelectionMode selectionMode{ SEL_IDLE };
PromptState promptState{ PromptState::Unknown };
@@ -148,6 +158,9 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
std::vector<Event> drainEvents();
std::vector<TerminalGraphicsUpdate> drainGraphicsUpdates();
void requestGraphicsResync();
void setSearchQuery( TerminalSearchQuery query );
void navigateSearch( int direction );
void clearSearch();
/** Bounded exact-selection request. Returns no value on timeout or during shutdown. */
std::optional<std::string>
@@ -220,6 +233,13 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
};
struct SelectionClearCommand {};
struct GraphicsResyncCommand {};
struct SearchQueryCommand {
TerminalSearchQuery query;
};
struct SearchNavigateCommand {
int direction{ 1 };
};
struct SearchClearCommand {};
struct ResetCommand {};
struct TerminateCommand {};
struct AllowTrimCommand : BoolCommand {};
@@ -233,7 +253,8 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
SelectionClearCommand, MouseCommand, FocusCommand, CursorModeCommand,
PaletteCommand, PresentationRateCommand, AllowTrimCommand, DataEventsCommand,
PromptEventsCommand, TerminateCommand, RestartCommand, ResetCommand,
SelectionRequestCommand, GraphicsResyncCommand>;
SelectionRequestCommand, GraphicsResyncCommand, SearchQueryCommand,
SearchNavigateCommand, SearchClearCommand>;
TerminalSession( PtyPtr&& pty, ProcPtr&& process, size_t historySize,
TerminalColorPalette palette );
@@ -261,6 +282,7 @@ class TerminalSession final : public std::enable_shared_from_this<TerminalSessio
std::shared_ptr<const TerminalSnapshot> mPublishedSnapshot;
std::atomic<bool> mShutdownRequested{ false };
std::atomic<Uint64> mNextScrollCommand{ 0 };
std::atomic<Uint64> mLatestSearchRequest{ 0 };
};
}} // namespace eterm::Terminal
@@ -135,6 +135,8 @@ enum TerminalGlyphAttribute {
ATTR_WDUMMY = 1 << 10,
ATTR_BOXDRAW = 1 << 11,
ATTR_EMOJI = 1 << 12,
ATTR_SEARCH_MATCH = 1 << 13,
ATTR_SEARCH_ACTIVE = 1 << 14,
ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
};
@@ -12,6 +12,8 @@ using namespace eterm::Terminal;
namespace eterm { namespace UI {
class UITerminalFind;
class UITerminal : public UIWidget {
public:
static UITerminal* New( Font* font, const Float& fontSize, const Sizef& pixelsSize,
@@ -135,6 +137,7 @@ class UITerminal : public UIWidget {
Clock mMouseClock;
std::shared_ptr<TerminalDisplay> mTerm;
Uint32 mTerminalEventCallbackId{ 0 };
UITerminalFind* mFindBar{ nullptr };
UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay );
@@ -0,0 +1,59 @@
#ifndef ETERM_UI_UITERMINALFIND_HPP
#define ETERM_UI_UITERMINALFIND_HPP
#include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uiselectbutton.hpp>
#include <eepp/ui/uitextinput.hpp>
#include <eepp/ui/uitextview.hpp>
using namespace EE;
using namespace EE::UI;
namespace eterm { namespace UI {
class UITerminal;
class UITerminalFind : public UILinearLayout {
public:
static UITerminalFind* New( UITerminal* terminal );
void show();
void hide();
void refreshStatus();
protected:
explicit UITerminalFind( UITerminal* terminal );
Uint32 onKeyDown( const KeyEvent& event );
Uint32 onKeyUp( const KeyEvent& event );
Uint32 onTextInput( const TextInputEvent& event );
Uint32 onTextEditing( const TextEditingEvent& event );
private:
void updateQuery();
void submitQuery();
void navigateSearch( int direction );
UITerminal* mTerminal{ nullptr };
UITextInput* mInput{ nullptr };
UISelectButton* mMatchCase{ nullptr };
UISelectButton* mWholeWord{ nullptr };
UISelectButton* mRegEx{ nullptr };
UISelectButton* mLuaPattern{ nullptr };
UITextView* mStatus{ nullptr };
Uint64 mRequestId{ 0 };
bool mReady{ false };
bool mChangingPattern{ false };
bool mQueryPending{ false };
};
}} // namespace eterm::UI
#endif
@@ -629,6 +629,33 @@ const std::shared_ptr<TerminalSession>& TerminalDisplay::getSession() const {
return mSession;
}
void TerminalDisplay::setSearchQuery( TerminalSearchQuery query ) {
if ( mSession )
mSession->setSearchQuery( std::move( query ) );
}
void TerminalDisplay::navigateSearch( int direction ) {
if ( mSession )
mSession->navigateSearch( direction );
}
void TerminalDisplay::clearSearch() {
if ( mSession )
mSession->clearSearch();
}
Uint32 TerminalDisplay::getSearchMatchCount() const {
return mSnapshot ? mSnapshot->searchMatchCount : 0;
}
Int32 TerminalDisplay::getCurrentSearchMatch() const {
return mSnapshot ? mSnapshot->currentSearchMatch : -1;
}
Uint64 TerminalDisplay::getSearchRequestId() const {
return mSnapshot ? mSnapshot->searchRequestId : 0;
}
int TerminalDisplay::scrollSize() const {
return mSnapshot ? mSnapshot->historyLength : 0;
}
@@ -1444,6 +1471,11 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
if ( glyph.mode & ATTR_REVERSE )
bg = fg;
if ( glyph.mode & ( ATTR_SEARCH_MATCH | ATTR_SEARCH_ACTIVE ) ) {
Color highlight = mColorScheme.getCursor();
highlight.a = glyph.mode & ATTR_SEARCH_ACTIVE ? 190 : 90;
bg = Color::blend( highlight, bg );
}
bool isWide = glyph.mode & ATTR_WIDE;
@@ -1498,6 +1530,11 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
}
if ( glyph.mode & ATTR_REVERSE )
background = foreground;
if ( glyph.mode & ( ATTR_SEARCH_MATCH | ATTR_SEARCH_ACTIVE ) ) {
Color highlight = mColorScheme.getCursor();
highlight.a = glyph.mode & ATTR_SEARCH_ACTIVE ? 190 : 90;
background = Color::blend( highlight, background );
}
const bool wide = glyph.mode & ATTR_WIDE;
const Float advance = spaceCharAdvanceX * ( wide ? 2.0f : 1.0f );
if ( background != defaultBg ) {
@@ -1555,6 +1592,11 @@ void TerminalDisplay::drawGrid( const Vector2f& pos ) {
fg = bg;
bg = temp;
}
if ( glyph.mode & ( ATTR_SEARCH_MATCH | ATTR_SEARCH_ACTIVE ) ) {
Color highlight = mColorScheme.getCursor();
highlight.a = glyph.mode & ATTR_SEARCH_ACTIVE ? 190 : 90;
bg = Color::blend( highlight, bg );
}
if ( glyph.mode & ATTR_BLINK && ( mMode & MODE_BLINK ) )
fg = bg;
@@ -586,6 +586,99 @@ int TerminalEmulator::tlinelen( Line line, int col ) const {
return i;
}
void TerminalEmulator::setSearchQuery( TerminalSearchQuery query ) {
TerminalSearchMatch previousMatch;
const bool preserveCurrent =
query.requestId == mSearchQuery.requestId && mCurrentSearchMatch >= 0 &&
mCurrentSearchMatch < static_cast<Int32>( mSearch.matches().size() );
if ( preserveCurrent )
previousMatch = mSearch.matches()[mCurrentSearchMatch];
mSearchQuery = std::move( query );
mSearchDirty = false;
mSearchRefreshClock.restart();
mSearchRows.clear();
if ( !TerminalSearch::isQuerySearchable( mSearchQuery ) ) {
mSearch.search( mSearchRows, mSearchQuery );
mCurrentSearchMatch = -1;
redraw();
return;
}
if ( tisaltscr() ) {
mSearchRows.reserve( mTerm.row );
for ( int row = 0; row < mTerm.row; ++row ) {
const int length = tlinelen( mTerm.line[row], mTerm.col );
mSearchRows.push_back(
{ mTerm.line[row], TerminalBufferSource::AlternateScreen, row, mTerm.col, length,
length > 0 && ( mTerm.line[row][length - 1].mode & ATTR_WRAP ) } );
}
} else {
mSearchRows.reserve( mTerm.histlen + mTerm.row );
for ( int row = -mTerm.histlen; row < mTerm.row; ++row ) {
Line line =
row < 0 ? mTerm.hist[( row + mTerm.histi + mTerm.histsize + 1 ) % mTerm.histsize]
: mTerm.line[row];
const int length = tlinelen( line, mTerm.col );
mSearchRows.push_back(
{ line,
row < 0 ? TerminalBufferSource::MainHistory : TerminalBufferSource::MainScreen,
row, mTerm.col, length, length > 0 && ( line[length - 1].mode & ATTR_WRAP ) } );
}
}
mSearch.search( mSearchRows, mSearchQuery );
mCurrentSearchMatch = mSearch.matches().empty() ? -1 : 0;
if ( preserveCurrent ) {
const auto& matches = mSearch.matches();
for ( size_t index = 0; index < matches.size(); ++index ) {
if ( matches[index].start.source == previousMatch.start.source &&
matches[index].start.row == previousMatch.start.row &&
matches[index].start.column == previousMatch.start.column ) {
mCurrentSearchMatch = static_cast<Int32>( index );
break;
}
}
}
if ( mCurrentSearchMatch >= 0 )
navigateSearch( 0 );
else
redraw();
}
void TerminalEmulator::navigateSearch( int direction ) {
const auto& matches = mSearch.matches();
if ( matches.empty() )
return;
if ( direction != 0 ) {
mCurrentSearchMatch =
( mCurrentSearchMatch + direction + static_cast<Int32>( matches.size() ) ) %
static_cast<Int32>( matches.size() );
}
const auto& match = matches[mCurrentSearchMatch];
if ( match.start.source == TerminalBufferSource::MainHistory ) {
TerminalArg scroll( eeclamp( static_cast<int>( -match.start.row ), 0, mTerm.histlen ) );
kscrollto( &scroll );
} else if ( match.start.source == TerminalBufferSource::MainScreen && mTerm.scr != 0 ) {
TerminalArg scroll( 0 );
kscrollto( &scroll );
}
redraw();
}
void TerminalEmulator::clearSearch() {
mSearchQuery = {};
mSearch.search( {}, mSearchQuery );
mCurrentSearchMatch = -1;
mSearchDirty = false;
redraw();
}
const std::vector<TerminalSearchMatch>& TerminalEmulator::getSearchMatches() const {
return mSearch.matches();
}
Int32 TerminalEmulator::getCurrentSearchMatch() const {
return mCurrentSearchMatch;
}
int TerminalEmulator::tiswrapped( int y ) {
int len = tlinelen( y );
@@ -4233,11 +4326,15 @@ void TerminalEmulator::resize( int columns, int rows, int pixelWidth, int pixelH
return;
}
tresize( columns, rows );
if ( !mSearchQuery.text.empty() )
setSearchQuery( mSearchQuery );
redraw();
return;
}
tresize( columns, rows );
if ( !mSearchQuery.text.empty() )
setSearchQuery( mSearchQuery );
redraw();
mPendingPtyColumns = columns;
@@ -4294,6 +4391,11 @@ bool TerminalEmulator::update() {
bool readBudgetSaturated =
reads == MAX_TTY_READS || presentationDeadlineReached ||
( reads > 0 && readBudgetClock.getElapsedTime() >= Milliseconds( 4 ) );
if ( reads > 0 && TerminalSearch::isQuerySearchable( mSearchQuery ) )
mSearchDirty = true;
if ( mSearchDirty &&
( !readBudgetSaturated || mSearchRefreshClock.getElapsedTime() >= Milliseconds( 100 ) ) )
setSearchQuery( mSearchQuery );
/* Keep presentation decoupled from every PTY read batch. Sustained output publishes on the
* host frame deadline, while a drained/idle burst still publishes immediately. */
@@ -0,0 +1,154 @@
#include <eterm/terminal/terminalsearch.hpp>
#include <eepp/system/luapattern.hpp>
#include <eepp/system/regex.hpp>
#include <optional>
using namespace EE::System;
namespace eterm { namespace Terminal {
static bool isWordCharacter( Rune rune ) {
return rune == '_' || String::isAlphaNum( rune );
}
template <typename Positions>
static void searchPattern( PatternMatcher& pattern, const std::string& text,
const String& logicalLine, const Positions& positions, bool wholeWord,
std::vector<TerminalSearchMatch>& matches ) {
if ( !pattern.isValid() )
return;
PatternMatcher::Range ranges[12];
int offset = 0;
size_t byteOffset = 0;
size_t codepointOffset = 0;
auto codepointPosition = [&]( size_t targetByteOffset ) {
while ( byteOffset < targetByteOffset ) {
if ( ( static_cast<unsigned char>( text[byteOffset] ) & 0xC0 ) != 0x80 )
++codepointOffset;
++byteOffset;
}
return codepointOffset;
};
while ( offset <= static_cast<int>( text.size() ) && pattern.matches( text, ranges, offset ) ) {
const int start = ranges[0].start;
const int end = ranges[0].end;
if ( start < 0 || end < start || end > static_cast<int>( text.size() ) )
break;
const size_t codepointStart = codepointPosition( start );
const size_t codepointEnd = codepointPosition( end );
if ( codepointStart < codepointEnd && codepointEnd <= positions.size() &&
( !wholeWord ||
( ( codepointStart == 0 || !isWordCharacter( logicalLine[codepointStart - 1] ) ) &&
( codepointEnd == logicalLine.size() ||
!isWordCharacter( logicalLine[codepointEnd] ) ) ) ) ) {
matches.push_back(
{ positions[codepointStart].start, positions[codepointEnd - 1].end } );
}
if ( end > offset ) {
offset = end;
} else if ( offset < static_cast<int>( text.size() ) ) {
do {
++offset;
} while ( offset < static_cast<int>( text.size() ) &&
( static_cast<unsigned char>( text[offset] ) & 0xC0 ) == 0x80 );
} else {
break;
}
}
}
void TerminalSearch::searchLogicalLine( const TerminalSearchQuery& query, PatternMatcher* pattern,
const String& needle ) {
if ( mText.empty() )
return;
if ( pattern ) {
mText.toUtf8( mUtf8Text );
searchPattern( *pattern, mUtf8Text, mText, mPositions, query.wholeWord, mMatches );
return;
}
const String* comparableText = &mText;
if ( !query.caseSensitive ) {
mComparableText = mText;
mComparableText.toLower();
comparableText = &mComparableText;
}
if ( needle.empty() || needle.size() > comparableText->size() )
return;
size_t offset = 0;
while ( ( offset = comparableText->find( needle, offset ) ) != String::InvalidPos ) {
if ( query.wholeWord && ( ( offset > 0 && isWordCharacter( mText[offset - 1] ) ) ||
( offset + needle.size() < mText.size() &&
isWordCharacter( mText[offset + needle.size()] ) ) ) ) {
++offset;
continue;
}
mMatches.push_back(
{ mPositions[offset].start, mPositions[offset + needle.size() - 1].end } );
++offset;
}
}
const std::vector<TerminalSearchMatch>&
TerminalSearch::search( const std::vector<TerminalSearchRowView>& rows,
const TerminalSearchQuery& query ) {
mMatches.clear();
mText.clear();
mPositions.clear();
if ( !isQuerySearchable( query ) )
return mMatches;
String needle;
if ( query.type == TerminalSearchType::Normal ) {
needle = query.text;
if ( !query.caseSensitive )
needle.toLower();
}
std::string patternText;
if ( query.type != TerminalSearchType::Normal )
query.text.toUtf8( patternText );
std::optional<RegEx> regex;
std::optional<LuaPatternStorage> luaPattern;
PatternMatcher* pattern = nullptr;
if ( query.type == TerminalSearchType::RegEx ) {
regex.emplace( patternText, static_cast<RegEx::Options>(
RegEx::Options::Utf | RegEx::Options::AllowFallback |
( query.caseSensitive ? RegEx::Options::None
: RegEx::Options::Caseless ) ) );
pattern = &*regex;
} else if ( query.type == TerminalSearchType::LuaPattern ) {
luaPattern.emplace( std::move( patternText ) );
pattern = &*luaPattern;
}
for ( const auto& row : rows ) {
if ( !row.cells || row.length < 0 || row.width < row.length )
continue;
for ( Int32 column = 0; column < row.length; ++column ) {
const auto& glyph = row.cells[column];
if ( glyph.mode & ATTR_WDUMMY )
continue;
mText += glyph.u;
mPositions.push_back(
{ { row.source, row.row, column },
{ row.source, row.row, column + ( ( glyph.mode & ATTR_WIDE ) ? 1 : 0 ) } } );
}
if ( !row.wrapped ) {
searchLogicalLine( query, pattern, needle );
mText.clear();
mPositions.clear();
}
}
if ( !mText.empty() )
searchLogicalLine( query, pattern, needle );
return mMatches;
}
}} // namespace eterm::Terminal
@@ -102,6 +102,36 @@ class TerminalSession::WorkerDisplay final : public ITerminalDisplay {
snapshot->exitCode = mEmulator->getExitCode();
snapshot->currentWorkingDirectory = mEmulator->getCurrentWorkingDirectory();
snapshot->promptState = mEmulator->getPromptState();
const auto& searchMatches = mEmulator->getSearchMatches();
snapshot->searchMatchCount = static_cast<Uint32>( searchMatches.size() );
snapshot->currentSearchMatch = mEmulator->getCurrentSearchMatch();
snapshot->searchRequestId = mEmulator->getSearchRequestId();
for ( size_t index = 0; index < searchMatches.size(); ++index ) {
const auto& match = searchMatches[index];
for ( Int64 row = match.start.row; row <= match.end.row; ++row ) {
const int visibleRow =
static_cast<int>( row ) +
( match.start.source == TerminalBufferSource::AlternateScreen
? 0
: snapshot->scrollPosition );
if ( visibleRow < 0 || visibleRow >= snapshot->rows )
continue;
const int startColumn = row == match.start.row ? match.start.column : 0;
const int endColumn =
row == match.end.row ? match.end.column : snapshot->columns - 1;
snapshot->visibleSearchMatches.push_back(
{ { startColumn, visibleRow },
{ endColumn, visibleRow },
static_cast<Int32>( index ) == snapshot->currentSearchMatch } );
for ( int column = std::max( 0, startColumn );
column <= std::min( snapshot->columns - 1, endColumn ); ++column ) {
auto& glyph = snapshot->cells[visibleRow * snapshot->columns + column];
glyph.mode |= static_cast<Int32>( index ) == snapshot->currentSearchMatch
? ATTR_SEARCH_ACTIVE
: ATTR_SEARCH_MATCH;
}
}
}
if ( auto* process = mEmulator->getProcess() )
snapshot->processId = process->pid();
}
@@ -415,6 +445,20 @@ void TerminalSession::requestGraphicsResync() {
enqueue( GraphicsResyncCommand{} );
}
void TerminalSession::setSearchQuery( TerminalSearchQuery query ) {
mLatestSearchRequest.store( query.requestId, std::memory_order_release );
enqueue( SearchQueryCommand{ std::move( query ) } );
}
void TerminalSession::navigateSearch( int direction ) {
enqueue( SearchNavigateCommand{ direction < 0 ? -1 : 1 } );
}
void TerminalSession::clearSearch() {
mLatestSearchRequest.store( 0, std::memory_order_release );
enqueue( SearchClearCommand{} );
}
std::shared_ptr<const TerminalSnapshot> TerminalSession::snapshot() const {
std::lock_guard<std::mutex> lock( mPublishedSnapshotMutex );
return mPublishedSnapshot;
@@ -606,6 +650,14 @@ void TerminalSession::processCommand( Command&& command ) {
} else if constexpr ( std::is_same_v<T, GraphicsResyncCommand> ) {
mGraphicsUpdates.resetResync();
mEmulator->requestGraphicsResync();
} else if constexpr ( std::is_same_v<T, SearchQueryCommand> ) {
if ( value.query.requestId ==
mLatestSearchRequest.load( std::memory_order_acquire ) )
mEmulator->setSearchQuery( std::move( value.query ) );
} else if constexpr ( std::is_same_v<T, SearchNavigateCommand> ) {
mEmulator->navigateSearch( value.direction );
} else if constexpr ( std::is_same_v<T, SearchClearCommand> ) {
mEmulator->clearSearch();
}
},
std::move( command ) );
+14 -1
View File
@@ -9,6 +9,7 @@
#include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp>
#include <eterm/ui/uiterminal.hpp>
#include <eterm/ui/uiterminalfind.hpp>
using namespace EE::Scene;
@@ -112,6 +113,11 @@ UITerminal::UITerminal( const std::shared_ptr<TerminalDisplay>& terminalDisplay
setCommand( "terminal-paste-selection",
[this] { mTerm->action( TerminalShortcutAction::PASTE_SELECTION ); } );
setCommand( "terminal-copy", [this] { mTerm->action( TerminalShortcutAction::COPY ); } );
mFindBar = UITerminalFind::New( this );
setCommand( "terminal-find", [this] { mFindBar->show(); } );
setCommand( "terminal-find-next", [this] { mTerm->navigateSearch( 1 ); } );
setCommand( "terminal-find-previous", [this] { mTerm->navigateSearch( -1 ); } );
setCommand( "terminal-find-close", [this] { mFindBar->hide(); } );
mKeyBindings.addKeybind( { KEY_V, KEYMOD_CTRL | KEYMOD_SHIFT }, "terminal-paste" );
mKeyBindings.addKeybind( { KEY_C, KEYMOD_CTRL | KEYMOD_SHIFT }, "terminal-copy" );
setCommand( "terminal-open-link",
@@ -345,6 +351,8 @@ void UITerminal::scheduledUpdate( const Time& ) {
bool mouseOutsideBounds =
mousePos.y < 0 || mousePos.y > getUISceneNode()->getWindow()->getSize().getHeight();
terminal->update( isMouseOverMeOrChildren() && !mouseOutsideBounds );
if ( mFindBar && mFindBar->isVisible() )
mFindBar->refreshStatus();
if ( !mVScroll->isDragging() && ( mScrollByBar || mPendingContentSizeChange ) ) {
updateScrollPosition();
if ( !mScrollByBar && mPendingContentSizeChange )
@@ -546,7 +554,7 @@ Uint32 UITerminal::onMouseDoubleClick( const Vector2i& position, const Uint32& f
}
Uint32 UITerminal::onMouseUp( const Vector2i& position, const Uint32& flags ) {
if ( flags & EE_BUTTON_RMASK ) {
if ( ( flags & EE_BUTTON_RMASK ) && !mTerm->isAppCapturingMouse() ) {
onCreateContextMenu( position, flags );
return 1;
}
@@ -567,6 +575,9 @@ void UITerminal::onSizeChange() {
( mViewType == ScrollViewType::Outside ? mVScroll->getPixelsSize().getWidth() : 0.f ),
mPaddingPx.Bottom } );
onContentSizeChange();
if ( mFindBar && mFindBar->isVisible() )
mFindBar->setPosition( eemax( 0.f, getSize().getWidth() - mFindBar->getSize().getWidth() ),
0 );
UIWidget::onSizeChange();
}
@@ -604,6 +615,8 @@ void UITerminal::createDefaultContextMenuOptions( UIPopUpMenu* menu ) {
->setEnabled( mTerm->hasSelection() );
menuAdd( menu, i18n( "uiterminal_paste", "Paste" ), "paste", "terminal-paste" )
->setEnabled( !getUISceneNode()->getWindow()->getClipboard()->getText().empty() );
menu->addSeparator();
menuAdd( menu, i18n( "uiterminal_find", "Find..." ), "search", "terminal-find" );
}
DrawablePtr UITerminal::findIcon( const std::string& name ) {
@@ -0,0 +1,209 @@
#include <eterm/ui/uiterminalfind.hpp>
#include <eepp/scene/actions/actions.hpp>
#include <eepp/ui/tools/uifindbarstyle.hpp>
#include <eepp/ui/uipushbutton.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eterm/ui/uiterminal.hpp>
using namespace EE::Scene;
using namespace EE::UI::Tools;
using namespace eterm::Terminal;
namespace eterm { namespace UI {
static constexpr auto SEARCH_DEBOUNCE_TAG = String::hash( "UITerminalFind::search" );
static constexpr char FIND_LAYOUT[] = R"xml(
<hbox class="ce_find_replace_box" layout_width="wrap_content" lh="wc" layout_gravity="right|top" margin_right="32dp">
<Widget class="expander" layout_width="2dp" layout_height="match_parent" />
<vbox layout_width="wrap_content" lh="wc">
<hbox layout_width="340dp" lh="wc" paddingRight="1dp" min-width="200dp" clip="border-box">
<RelativeLayout layout_width="0dp" layout_weight="1" layout_height="25dp" marginRight="1dp">
<TextInput class="input-find" layout_width="match_parent" lh="wc" hint="@string(find, Find)" />
<hbox lw="wc" lh="wc" layout_gravity="right|center_vertical">
<TextView class="status" text="0/0" lw="wc" min-width="32dp" lh="wc" layout_gravity="right|center_vertical"
gravity="right" marginRight="2dp" font-family="monospace" />
<selectbutton id="terminal-find-luapattern" class="luapattern" lw="wc" lh="wc"
layout_gravity="right|center_vertical" tooltip="@string(lua_pattern_match, Lua Pattern Match)" marginRight="2dp" />
<selectbutton id="terminal-find-regex" class="regex" lw="wc" lh="wc"
layout_gravity="right|center_vertical" tooltip="@string(regex_match, Regular Expression Match)" marginRight="2dp" />
<selectbutton id="terminal-find-whole-word" class="whole-word" lw="wc" lh="wc"
layout_gravity="right|center_vertical" tooltip="@string(whole_word, Whole Word)" marginRight="2dp" />
<selectbutton id="terminal-find-match-case" class="match-case" lw="wc" lh="wc"
layout_gravity="right|center_vertical" tooltip="@string(match_case, Match Case)" marginRight="2dp" />
</hbox>
</RelativeLayout>
<PushButton class="prev-button" lw="wc" layout_height="24dp" text="@string(prev, Prev.)"
icon="icon(arrow-up, 16dp)" />
<PushButton class="next-button" lw="wc" layout_height="24dp" text="@string(next, Next)"
icon="icon(arrow-down, 16dp)" />
<PushButton class="exit-button" lw="wc" layout_height="24dp" text="@string(close, Close)"
icon="icon(cancel, 16dp)" />
</hbox>
</vbox>
</hbox>
)xml";
UITerminalFind* UITerminalFind::New( UITerminal* terminal ) {
return eeNew( UITerminalFind, ( terminal ) );
}
UITerminalFind::UITerminalFind( UITerminal* terminal ) :
UILinearLayout( "terminalfind", UIOrientation::Horizontal ), mTerminal( terminal ) {
mFlags |= UI_OWNS_CHILDREN_POSITION;
UIFindBarStyle::ensure( terminal->getUISceneNode() );
terminal->getUISceneNode()->loadLayoutFromMemory( FIND_LAYOUT, sizeof( FIND_LAYOUT ), this );
setParent( terminal );
mInput = querySelector<UITextInput>( ".ce_find_replace_box .input-find" );
mMatchCase = querySelector<UISelectButton>( ".ce_find_replace_box .match-case" );
mWholeWord = querySelector<UISelectButton>( ".ce_find_replace_box .whole-word" );
mRegEx = querySelector<UISelectButton>( ".ce_find_replace_box .regex" );
mLuaPattern = querySelector<UISelectButton>( ".ce_find_replace_box .luapattern" );
mStatus = querySelector<UITextView>( ".ce_find_replace_box .status" );
mInput->on( Event::OnTextChanged, [this]( const Event* ) { updateQuery(); } );
mInput->on( Event::OnPressEnter, [this]( const Event* ) { navigateSearch( 1 ); } );
mMatchCase->on( Event::OnValueChange, [this]( const Event* ) { updateQuery(); } );
mWholeWord->on( Event::OnValueChange, [this]( const Event* ) { updateQuery(); } );
mRegEx->on( Event::OnValueChange, [this]( const Event* ) {
if ( mChangingPattern )
return;
mChangingPattern = true;
mLuaPattern->setSelected( false );
mChangingPattern = false;
updateQuery();
} );
mLuaPattern->on( Event::OnValueChange, [this]( const Event* ) {
if ( mChangingPattern )
return;
mChangingPattern = true;
mRegEx->setSelected( false );
mChangingPattern = false;
updateQuery();
} );
querySelector( ".ce_find_replace_box .prev-button" )
->on( Event::MouseClick, [this]( const Event* event ) {
if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
navigateSearch( -1 );
} );
querySelector( ".ce_find_replace_box .next-button" )
->on( Event::MouseClick, [this]( const Event* event ) {
if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
navigateSearch( 1 );
} );
querySelector( ".ce_find_replace_box .exit-button" )
->on( Event::MouseClick, [this]( const Event* event ) {
if ( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK )
hide();
} );
setVisible( false );
runOnMainThread( [this] { mReady = true; } );
}
void UITerminalFind::show() {
if ( !mReady ) {
runOnMainThread( [this] { show(); } );
return;
}
if ( !isVisible() ) {
setVisible( true );
const Float startX = eemax( 0.f, mTerminal->getSize().getWidth() - getSize().getWidth() );
setPosition( startX, -getSize().getHeight() );
runAction( Actions::Move::New( { startX, getPosition().y }, { startX, 0 }, Seconds( 0.2f ),
Ease::QuadraticIn ) );
}
mInput->getDocument().selectAll();
mInput->setFocus();
if ( !mInput->getText().empty() )
updateQuery();
}
void UITerminalFind::hide() {
mInput->removeActionsByTag( SEARCH_DEBOUNCE_TAG );
mQueryPending = false;
runAction( Actions::Sequence::New(
Actions::Move::New( getPosition(), { getPosition().x, -getSize().getHeight() },
Seconds( 0.2f ), Ease::QuadraticOut ),
Actions::Visible::New( false ) ) );
mTerminal->getTerm()->clearSearch();
mTerminal->setFocus();
}
void UITerminalFind::updateQuery() {
mInput->removeActionsByTag( SEARCH_DEBOUNCE_TAG );
if ( mInput->getText().size() < TerminalSearch::MinimumQueryLength ) {
submitQuery();
return;
}
mQueryPending = true;
mInput->debounce( [this] { submitQuery(); }, Milliseconds( 150 ), SEARCH_DEBOUNCE_TAG );
}
void UITerminalFind::submitQuery() {
mInput->removeActionsByTag( SEARCH_DEBOUNCE_TAG );
mQueryPending = false;
TerminalSearchType type = TerminalSearchType::Normal;
if ( mRegEx->isSelected() )
type = TerminalSearchType::RegEx;
else if ( mLuaPattern->isSelected() )
type = TerminalSearchType::LuaPattern;
mTerminal->getTerm()->setSearchQuery( { mInput->getText(), ++mRequestId,
mMatchCase->isSelected(), mWholeWord->isSelected(),
type } );
}
void UITerminalFind::navigateSearch( int direction ) {
if ( mQueryPending ) {
submitQuery();
return;
}
mTerminal->getTerm()->navigateSearch( direction );
}
void UITerminalFind::refreshStatus() {
if ( mTerminal->getTerm()->getSearchRequestId() != mRequestId )
return;
const Uint32 count = mTerminal->getTerm()->getSearchMatchCount();
const Int32 current = mTerminal->getTerm()->getCurrentSearchMatch();
mStatus->setText( String::format( "%d/%u", current >= 0 ? current + 1 : 0, count ) );
if ( count == 0 && !mInput->getText().empty() )
mInput->addClass( "error" );
else
mInput->removeClass( "error" );
}
Uint32 UITerminalFind::onKeyDown( const KeyEvent& event ) {
if ( event.getKeyCode() == KEY_ESCAPE ) {
hide();
return 1;
}
if ( event.getKeyCode() == KEY_RETURN || event.getKeyCode() == KEY_KP_ENTER ) {
navigateSearch( event.getMod() & KEYMOD_SHIFT ? -1 : 1 );
return 1;
}
const std::string command =
mTerminal->getKeyBindings().getCommandFromKeyBind( { event.getKeyCode(), event.getMod() } );
if ( command == "terminal-find-next" )
navigateSearch( 1 );
else if ( command == "terminal-find-previous" )
navigateSearch( -1 );
else if ( command == "terminal-find-close" )
hide();
else if ( command == "terminal-find" )
show();
return 1;
}
Uint32 UITerminalFind::onKeyUp( const KeyEvent& ) {
return 1;
}
Uint32 UITerminalFind::onTextInput( const TextInputEvent& ) {
return 1;
}
Uint32 UITerminalFind::onTextEditing( const TextEditingEvent& ) {
return 1;
}
}} // namespace eterm::UI
+92 -1
View File
@@ -10,6 +10,7 @@
#include <eterm/terminal/iterminaldisplay.hpp>
#include <eterm/terminal/terminalemulator.hpp>
#include <eterm/terminal/terminalgraphics.hpp>
#include <eterm/terminal/terminalsearch.hpp>
#include <eterm/terminal/terminalsession.hpp>
#include <limits>
#include <thread>
@@ -1343,8 +1344,12 @@ UTEST( eterm, sgr_pixel_mouse_mode_uses_grid_relative_pixels ) {
term->update();
term->mousereport( TerminalMouseEventType::MouseButtonDown, { 2, 3 }, { 20, 30 },
EE_BUTTON_LMASK, 0 );
term->mousereport( TerminalMouseEventType::MouseButtonDown, { 2, 3 }, { 20, 30 },
EE_BUTTON_RMASK, 0 );
term->mousereport( TerminalMouseEventType::MouseButtonRelease, { 2, 3 }, { 20, 30 },
EE_BUTTON_RMASK, 0 );
EXPECT_STDSTREQ( "\033[<0;21;31M", ptyPtr->mWrites );
EXPECT_STDSTREQ( "\033[<0;21;31M\033[<2;21;31M\033[<2;21;31m", ptyPtr->mWrites );
}
UTEST( eterm, cursor_style_zero_uses_blinking_configured_shape ) {
@@ -2327,3 +2332,89 @@ UTEST( eterm, history_corruption_on_resize ) {
EXPECT_STDSTREQ( expected_lines[expected_idx], sel );
}
}
UTEST( eterm_search, logical_lines_options_and_cell_mapping ) {
TerminalGlyph first[] = { { 'H' }, { 'e' }, { 'l' }, { 'l' }, { 'o', ATTR_WRAP } };
TerminalGlyph second[] = { { 'W' }, { 'o' }, { 'r' }, { 'l' }, { 'd' } };
TerminalGlyph wide[] = { { 0x754C, ATTR_WIDE }, { 0, ATTR_WDUMMY }, { '!' } };
std::vector<TerminalSearchRowView> rows{
{ first, TerminalBufferSource::MainHistory, -1, 5, 5, true },
{ second, TerminalBufferSource::MainScreen, 0, 5, 5, false },
{ wide, TerminalBufferSource::MainScreen, 1, 3, 3, false },
};
TerminalSearch search;
EXPECT_TRUE( search.search( rows, { "o", 0, true, false } ).empty() );
auto matches = search.search( rows, { "lowo", 1, false, false } );
ASSERT_EQ( static_cast<size_t>( 1 ), matches.size() );
EXPECT_EQ( static_cast<Int64>( -1 ), matches[0].start.row );
EXPECT_EQ( 3, matches[0].start.column );
EXPECT_EQ( static_cast<Int64>( 0 ), matches[0].end.row );
EXPECT_EQ( 1, matches[0].end.column );
EXPECT_TRUE( search.search( rows, { "hello", 2, true, true } ).empty() );
EXPECT_EQ( static_cast<size_t>( 1 ),
search.search( rows, { "HELLOWORLD", 3, false, true } ).size() );
String unicodeQuery;
unicodeQuery += static_cast<String::StringBaseType>( 0x754C );
unicodeQuery += '!';
matches = search.search( rows, { unicodeQuery, 4, true, false } );
ASSERT_EQ( static_cast<size_t>( 1 ), matches.size() );
EXPECT_EQ( 0, matches[0].start.column );
EXPECT_EQ( 2, matches[0].end.column );
matches = search.search( rows, { "l+oW.rld", 5, true, false, TerminalSearchType::RegEx } );
ASSERT_EQ( static_cast<size_t>( 1 ), matches.size() );
EXPECT_EQ( 2, matches[0].start.column );
EXPECT_EQ( static_cast<Int64>( 0 ), matches[0].end.row );
EXPECT_EQ( 4, matches[0].end.column );
matches = search.search( rows, { "界!", 6, true, false, TerminalSearchType::RegEx } );
ASSERT_EQ( static_cast<size_t>( 1 ), matches.size() );
EXPECT_EQ( 0, matches[0].start.column );
EXPECT_EQ( 2, matches[0].end.column );
matches = search.search( rows, { "[Ww]%a+d", 7, true, false, TerminalSearchType::LuaPattern } );
ASSERT_EQ( static_cast<size_t>( 1 ), matches.size() );
EXPECT_EQ( 0, matches[0].start.column );
EXPECT_EQ( 4, matches[0].end.column );
EXPECT_TRUE(
search.search( rows, { "[", 8, true, false, TerminalSearchType::RegEx } ).empty() );
EXPECT_EQ( static_cast<size_t>( 1 ),
search.search( rows, { "helloworld", 9, false, false, TerminalSearchType::RegEx } )
.size() );
}
UTEST( eterm_search, emulator_history_navigation_and_clear ) {
auto pty = std::make_unique<MockPty>();
auto process = std::make_unique<MockProcess>();
auto display = std::make_shared<MockDisplay>();
auto term = TerminalEmulator::create( std::move( pty ), std::move( process ), display, 100 );
ASSERT_TRUE( term != nullptr );
for ( int line = 0; line < 40; ++line ) {
const std::string text = "line " + std::to_string( line ) + " needle\r\n";
term->write( text.data(), text.size() );
term->update();
}
term->setSearchQuery( { "n", 41, true, false } );
EXPECT_TRUE( term->getSearchMatches().empty() );
EXPECT_EQ( -1, term->getCurrentSearchMatch() );
EXPECT_EQ( static_cast<Uint64>( 41 ), term->getSearchRequestId() );
term->setSearchQuery( { "needle", 42, true, true } );
ASSERT_EQ( static_cast<size_t>( 40 ), term->getSearchMatches().size() );
EXPECT_EQ( 0, term->getCurrentSearchMatch() );
EXPECT_TRUE( term->scrollPos() > 0 );
term->navigateSearch( -1 );
EXPECT_EQ( 39, term->getCurrentSearchMatch() );
EXPECT_EQ( 0, term->scrollPos() );
term->navigateSearch( 1 );
EXPECT_EQ( 0, term->getCurrentSearchMatch() );
term->clearSearch();
EXPECT_TRUE( term->getSearchMatches().empty() );
EXPECT_EQ( -1, term->getCurrentSearchMatch() );
}
@@ -100,6 +100,13 @@ UTEST( String, reusableFormattingAndUtf8Assignment ) {
const std::string utf8Text = text.toUtf8();
EXPECT_STREQ( "áβ中", utf8Text.c_str() );
EXPECT_EQ( textStorage, text.getString().data() );
std::string reusableUtf8;
reusableUtf8.reserve( 128 );
const char* utf8Storage = reusableUtf8.data();
text.toUtf8( reusableUtf8 );
EXPECT_STREQ( "áβ中", reusableUtf8.c_str() );
EXPECT_EQ( utf8Storage, reusableUtf8.data() );
}
UTEST( FileSystem, fileCountLines ) {
+7 -1
View File
@@ -133,6 +133,10 @@ void TerminalManager::loadTerminalColorSchemes() {
KeyBindings::ShortcutMap TerminalManager::getTerminalKeybindings() {
return {
{ { KEY_F, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "terminal-find" },
{ { KEY_G, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "terminal-find-next" },
{ { KEY_G, KeyMod::getDefaultModifier() | KEYMOD_SHIFT | KEYMOD_ALT },
"terminal-find-previous" },
{ { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "create-new-terminal" },
{ { KEY_E,
KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT },
@@ -741,7 +745,6 @@ void TerminalManager::setKeybindings( UITerminal* term ) {
term->getKeyBindings().reset();
term->addKeyBinds( mApp->getRealLocalKeybindings() );
term->addKeyBinds( mApp->getRealSplitterKeybindings() );
term->addKeyBinds( mApp->getRealTerminalKeybindings() );
// Remove the keybinds that are problematic for a terminal
term->getKeyBindings().removeCommandsKeybind(
{ "open-file", "download-file-web", "open-folder", "debug-draw-highlight-toggle",
@@ -749,6 +752,9 @@ void TerminalManager::setKeybindings( UITerminal* term ) {
"open-locatebar", "open-command-palette", "open-global-search", "menu-toggle",
"console-toggle", "go-to-line", "editor-go-back", "editor-go-forward",
"project-run-executable", "project-build-and-run" } );
// Terminal bindings must be installed last so they can intentionally reuse shortcuts removed
// from the editor/global context (for example mod+shift+f).
term->addKeyBinds( mApp->getRealTerminalKeybindings() );
}
} // namespace ecode
+7 -1
View File
@@ -3,6 +3,10 @@
namespace eterm {
struct TabWidgetSplitterDeleter {
void operator()( UITabWidgetSplitter* splitter ) const { eeDelete( splitter ); }
};
void App::TerminalSplitterClient::onTabCreated( UITab* tab, UIWidget* widget ) {
if ( mApp.terminalIcon && widget && widget->isType( UI_TYPE_TERMINAL ) )
tab->setIcon( mApp.terminalIcon->createDrawable( PixelDensity::dpToPxI( 12 ) ) );
@@ -825,7 +829,9 @@ int App::run( int argc, char* argv[] ) {
mainLayout->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent );
mainLayout->setPixelsSize( appWindow->getSize().asFloat() );
tabSplitter = UITabWidgetSplitter::New( &splitterClient, scene );
std::unique_ptr<UITabWidgetSplitter, TabWidgetSplitterDeleter> tabSplitterOwner(
UITabWidgetSplitter::New( &splitterClient, scene ) );
tabSplitter = tabSplitterOwner.get();
tabSplitter->setHideTabBarOnSingleTab( !config->window.alwaysShowTabBar );
tabSplitter->setCanCreateSplitFn( [this]( SplitDirection, UIWidget* ) {
restoreMaximizedTabWidget();
+4
View File
@@ -10,6 +10,10 @@ KeyBindings::ShortcutMap App::getDefaultKeybindings() const {
return {
{ { KEY_C, KEYMOD_CTRL | KEYMOD_SHIFT }, "terminal-copy" },
{ { KEY_V, KEYMOD_CTRL | KEYMOD_SHIFT }, "terminal-paste" },
{ { KEY_F, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "terminal-find" },
{ { KEY_G, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "terminal-find-next" },
{ { KEY_G, KeyMod::getDefaultModifier() | KEYMOD_SHIFT | KEYMOD_ALT },
"terminal-find-previous" },
{ { KEY_T, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "create-new-terminal" },
{ { KEY_E,
KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT },