• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

antonvw / wex / 17877007475

20 Sep 2025 07:22AM UTC coverage: 64.153% (+5.5%) from 58.646%
17877007475

push

github

antonvw
Merge branch 'develop'

18261 of 31235 branches covered (58.46%)

Branch coverage included in aggregate %.

14571 of 19943 relevant lines covered (73.06%)

1401.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

24.7
/src/vcs/debug.cpp
1
////////////////////////////////////////////////////////////////////////////////
2
// Name:      debug.cpp
3
// Purpose:   Implementation of class wex::debug
4
// Author:    Anton van Wezenbeek
5
// Copyright: (c) 2016-2025 Anton van Wezenbeek
6
////////////////////////////////////////////////////////////////////////////////
7

8
#include <boost/tokenizer.hpp>
9
#include <wex/core/wex.h>
10
#include <wex/factory/wex.h>
11
#include <wex/stc/stc.h>
12
#include <wex/syntax/util.h>
13
#include <wex/ui/wex.h>
14
#include <wex/vcs/debug.h>
15

16
#include <algorithm>
17
#include <charconv>
18
#include <fstream>
19

20
#ifdef __WXGTK__
21
#include <wex/common/dir.h>
22

23
namespace wex
24
{
25
// This class adds name and pid of running processes to
26
// a listview. Each process has an entry in /proc,
27
// with a subdir of the pid as name. In this dir there is
28
// a file comm that contains the name of that process.
29
class process_dir : public dir
30
{
31
public:
32
  process_dir(listview* lv, bool init)
×
33
    : dir(
×
34
        path("/proc"),
×
35
        data::dir().file_spec("[0-9]+").type(
×
36
          data::dir::type_t().set(data::dir::DIRS)))
×
37
    , m_listview(lv)
×
38
  {
39
    if (init)
×
40
    {
41
      m_listview->append_columns({{"Name", column::STRING_MEDIUM}, {"Pid"}});
×
42
    }
43
    else
44
    {
45
      m_listview->clear();
×
46
    }
47
  }
×
48

49
  ~process_dir() { m_listview->sort_column("Name", SORT_ASCENDING); }
×
50

51
private:
52
  bool on_dir(const path& p) const final
×
53
  {
54
    if (!std::filesystem::is_symlink(p.data()))
×
55
    {
56
      std::ifstream ifs(wex::path(p.data(), "comm").data());
×
57
      if (std::string line; ifs.is_open() && std::getline(ifs, line))
×
58
      {
59
        m_listview->insert_item({line, p.name()});
×
60
      }
×
61
    }
×
62
    return true;
×
63
  }
×
64

65
  listview* m_listview;
66
};
67
}; // namespace wex
68
#endif
69

70
#define MATCH(REGEX)                                                           \
71
  regex v(m_entry.regex_stdout(debug_entry::regex_t::REGEX));                  \
72
  v.search(m_stdout)
73

74
std::string wex::debug::default_exe()
55✔
75
{
76
  return
77
#ifdef __WXOSX__
78
    "lldb";
79
#else
80
    "gdb";
110✔
81
#endif
82
}
83

84
wex::debug::debug(wex::frame* frame, wex::factory::process* debug)
47✔
85
  : m_frame(frame)
47✔
86
  , m_process(debug)
47✔
87
{
88
  set_entry(config("debug.debugger").get(default_exe()));
47✔
89

90
  bind(this).command(
282!
91
    {{[=, this](const wxCommandEvent& event)
×
92
      {
93
        is_finished();
×
94
      },
×
95
      ID_DEBUG_EXIT},
×
96
     {[=, this](const wxCommandEvent& event)
×
97
      {
98
        process_stdin(event.GetString());
×
99
      },
×
100
      ID_DEBUG_STDIN},
×
101
     {[=, this](const wxCommandEvent& event)
×
102
      {
103
        process_stdout(event.GetString());
×
104
      },
×
105
      ID_DEBUG_STDOUT}});
×
106
}
94!
107

108
int wex::debug::add_menu(wex::menu* menu, bool popup) const
3✔
109
{
110
  if (popup && m_process == nullptr)
3✔
111
  {
112
    return 0;
1✔
113
  }
114

115
  wex::menu* sub = (popup ? new wex::menu : nullptr);
5!
116
  wex::menu* use = (popup ? sub : menu);
2✔
117

118
  if (popup)
2✔
119
  {
120
    use->style().set(menu::IS_POPUP);
1✔
121
  }
122

123
  const auto ret =
124
    wex::menus::build_menu(m_entry.get_commands(), ID_EDIT_DEBUG_FIRST, use);
2✔
125

126
  if (ret > 0 && popup)
2!
127
  {
128
    menu->append({{}, {sub, "debug"}});
6!
129
  }
130

131
  return ret;
2✔
132
}
3!
133

134
bool wex::debug::allow_open(const path& p) const
×
135
{
136
  return p.file_exists() &&
×
137
         matches_one_of(p.extension(), debug_entry().extensions());
×
138
}
139

140
bool wex::debug::apply_breakpoints(stc* stc) const
1✔
141
{
142
  bool found = false;
1✔
143

144
  for (const auto& it : m_breakpoints)
1!
145
  {
146
    if (std::get<0>(it.second) == stc->path())
×
147
    {
148
      stc->MarkerAdd(std::get<2>(it.second), m_marker_breakpoint.number());
×
149
      found = true;
×
150
    }
151
  }
152

153
  return found;
1✔
154
}
155

156
bool wex::debug::clear_breakpoints(const std::string& text)
2✔
157
{
158
  if (regex v("(d|del|delete|Delete) (all )?breakpoints"); v.search(text) >= 1)
4!
159
  {
160
    for (const auto& it : m_breakpoints)
×
161
    {
162
      if (m_frame->is_open(std::get<0>(it.second)))
×
163
      {
164
        auto* stc = m_frame->open_file(std::get<0>(it.second));
×
165
        stc->MarkerDeleteAll(m_marker_breakpoint.number());
×
166
      }
167
    }
168

169
    m_breakpoints.clear();
×
170

171
    return true;
×
172
  }
2!
173

174
  return false;
2✔
175
}
176

177
wex::path wex::debug::complete_path(const std::string& text) const
×
178
{
179
  if (path p(text); p.is_absolute())
×
180
  {
181
    return p;
×
182
  }
×
183

184
  return path(wex::path(m_path.parent_path()), text);
×
185
}
186

187
bool wex::debug::execute(const std::string& action, wex::stc* stc)
2✔
188
{
189
  const auto& r(get_args(action, stc));
2✔
190
  if (!r)
2!
191
  {
192
    return false;
×
193
  }
194

195
  const auto& exe(
196
    m_entry.name() + (!m_entry.flags().empty() ?
2!
197
                        std::string(1, ' ') + m_entry.flags() :
2!
198
                        std::string()));
2✔
199

200
  log::trace("debug exe") << exe << *r;
2✔
201

202
  if (
2✔
203
    m_process == nullptr &&
3✔
204
    ((m_process = m_frame->get_process(exe)) == nullptr))
1!
205
  {
206
    log("debug") << m_entry.name() << "no process";
1✔
207
    return false;
1✔
208
  }
209

210
  if (!m_process->is_running() && !m_process->async_system(exe))
1!
211
  {
212
    log("debug") << m_entry.name() << "process no execute" << exe;
×
213
    return false;
×
214
  }
215

216
  if (regex v(" +([a-zA-Z0-9_./-]*)"); v.search(*r) == 1)
2!
217
  {
218
    m_path = path(v[0]);
×
219
  }
1✔
220

221
  return m_process->write(
1✔
222
    action == "interrupt" ? std::string(1, 3) : action + *r);
2!
223
}
2✔
224

225
bool wex::debug::execute(int item, stc* stc)
1✔
226
{
227
  return item >= 0 && item < static_cast<int>(m_entry.get_commands().size()) &&
1!
228
         execute(m_entry.get_commands().at(item).get_command(), stc);
1!
229
};
230

231
std::optional<std::string>
232
wex::debug::get_args(const std::string& command, stc* stc)
2✔
233
{
234
  std::string args;
2✔
235

236
  if (regex v("^(at\\b|attach)(( +)(.*))?"); v.search(command) >= 1)
4!
237
  {
238
    if (v.size() > 1 && !v[1].empty())
×
239
    {
240
      if (int result{};
×
241
          std::from_chars(v[1].data(), v[1].data() + v[1].size(), result).ec !=
×
242
          std::errc())
243
      {
244
        return {};
×
245
      }
246

247
      args = command;
×
248
    }
249
    else
250
    {
251
      static listview* lv   = nullptr;
252
      bool             init = false;
×
253

254
      if (m_dialog == nullptr)
×
255
      {
256
        init     = true;
×
257
        m_dialog = new item_dialog(
×
258
          {
259
#ifdef __WXGTK__
260
            {"debug.processes",
261
             data::listview(),
×
262
             std::any(),
×
263
             data::item()
×
264
               .label_type(data::item::LABEL_NONE)
×
265
               .apply(
×
266
                 [&](wxWindow* user, const std::any& value, bool save)
×
267
                 {
268
                   lv = ((wex::listview*)user);
×
269
                   if (save && lv->GetFirstSelected() != -1)
×
270
                   {
271
                     args =
272
                       " " + lv->get_item_text(lv->GetFirstSelected(), "Pid");
×
273
                   }
274
                 })}},
×
275
#else
276
            {"debug.pid",
277
             item::TEXTCTRL_INT,
278
             std::any(),
279
             data::item(data::control().is_required(true))
280
               .label_type(data::item::LABEL_LEFT)
281
               .apply(
282
                 [&](wxWindow* user, const std::any& value, bool save)
283
                 {
284
                   if (save)
285
                     args += " " + std::to_string(std::any_cast<long>(value));
286
                 })}},
287
#endif
288
          data::window().title(_("Attach")).size({400, 400}).parent(m_frame));
×
289
      }
290

291
#ifdef __WXGTK__
292
      if (lv != nullptr)
×
293
      {
294
        process_dir(lv, init).find_files();
×
295
      }
296
#endif
297
    }
298
    return m_dialog->ShowModal() == wxID_CANCEL ?
×
299
             std::nullopt :
300
             std::optional<std::string>{args};
×
301
  }
2!
302
  if (regex r("^(b|break)"); r.search(command) == 1)
4!
303
  {
304
    if (stc == nullptr)
×
305
    {
306
      return {};
×
307
    }
308

309
    args += " " + stc->path().string() + ":" +
×
310
            std::to_string(stc->get_current_line() + 1);
×
311
  }
312
  else if (regex r("^(d|del|delete) (br|breakpoint)"); r.search(command) > 0)
4!
313
  {
314
    if (stc == nullptr)
×
315
    {
316
      return {};
×
317
    }
318

319
    for (auto& it : m_breakpoints)
×
320
    {
321
      if (
×
322
        stc->path() == std::get<0>(it.second) &&
×
323
        stc->get_current_line() == std::get<2>(it.second))
×
324
      {
325
        args += " " + it.first;
×
326
        stc->MarkerDeleteHandle(std::get<1>(it.second));
×
327
        m_breakpoints.erase(it.first);
×
328
        break;
×
329
      }
330
    }
331
  }
332
  else if (clear_breakpoints(command))
2!
333
  {
334
  }
335
  else if (regex r("^(detach)"); r.search(command) == 1)
4!
336
  {
337
    is_finished();
×
338
  }
339
  else if (command == "file")
2!
340
  {
341
    return item_dialog(
×
342
             {{_("debug.File"),
×
343
               item::COMBOBOX_FILE,
344
               std::any(),
×
345
               data::item(data::control().is_required(true))
×
346
                 .apply(
×
347
                   [&](wxWindow* user, const std::any& value, bool save)
×
348
                   {
349
                     if (save)
×
350
                     {
351
                       args += " " + std::any_cast<wxArrayString>(value)[0];
×
352
                     }
353
                   })},
×
354
              {"debug." + m_entry.name(), item::FILEPICKERCTRL}},
×
355
             data::window().title(_("Debug")).parent(m_frame))
×
356
                 .ShowModal() != wxID_CANCEL ?
×
357
             std::optional<std::string>{args} :
358
             std::nullopt;
×
359
  }
360
  else if (regex r("^(p|print)"); r.search(command) == 1)
4!
361
  {
362
    if (stc != nullptr)
×
363
    {
364
      args += " " + stc->GetSelectedText();
×
365
    }
366
  }
367
  else if (regex r("^(u|until|thread until)");
2✔
368
           r.search(command) == 1 && stc != nullptr)
2!
369
  {
370
    args += " " + std::to_string(stc->get_current_line());
×
371
  }
10!
372

373
  return {args};
2✔
374
}
2!
375

376
bool wex::debug::is_active() const
7✔
377
{
378
  return m_process != nullptr && m_process->is_running();
7✔
379
}
380

381
void wex::debug::is_finished()
×
382
{
383
  log::trace("debug") << m_entry.name() << "finished";
×
384

385
  if (!m_frame->is_closing() && allow_open(m_path_execution_point))
×
386
  {
387
    if (auto* stc = m_frame->open_file(m_path_execution_point); stc != nullptr)
×
388
    {
389
      stc->SetIndicatorCurrent(data::stc::IND_DEBUG);
×
390
      stc->IndicatorClearRange(0, stc->GetTextLength() - 1);
×
391
      stc->AnnotationClearAll();
×
392
    }
393
  }
394
}
×
395

396
bool wex::debug::print(const std::string& variable) const
2✔
397
{
398
  return is_active() && m_process->write("print " + variable);
2!
399
}
400

401
void wex::debug::process_stdin(const std::string& text)
×
402
{
403
  log::trace("debug stdin") << text;
×
404

405
  // parse delete a breakpoint with text, numbers
406
  if (regex v("(d|del|delete) +([0-9 ]*)"); v.search(text) > 0)
×
407
  {
408
    switch (v.size())
×
409
    {
410
      case 1:
×
411
        clear_breakpoints(text);
×
412
        break;
×
413

414
      case 2:
×
415
        for (const auto& token : boost::tokenizer<boost::char_separator<char>>(
×
416
               v[1],
×
417
               boost::char_separator<char>(" ")))
×
418
        {
419
          if (const auto& it = m_breakpoints.find(token);
×
420
              it != m_breakpoints.end() &&
×
421
              m_frame->is_open(std::get<0>(it->second)))
×
422
          {
423
            if (auto* stc = m_frame->open_file(std::get<0>(it->second));
×
424
                stc != nullptr)
425
            {
426
              stc->MarkerDeleteHandle(std::get<1>(it->second));
×
427
            }
428
          }
429
        }
×
430
        break;
×
431
    }
432
  }
×
433
}
×
434

435
void wex::debug::process_stdout(const std::string& text)
×
436
{
437
  m_stdout += text;
×
438

439
  log::trace("debug stdout") << m_stdout << m_path;
×
440
  data::stc data;
×
441

442
  if (MATCH(BREAKPOINT_NO_FILE_LINE) == 3)
×
443
  {
444
    m_stdout.clear();
×
445

446
    if (const auto& filename(complete_path(v[1])); allow_open(filename))
×
447
    {
448
      if (auto* stc = m_frame->open_file(filename); stc != nullptr)
×
449
      {
450
        const auto line = std::stoi(v[2]) - 1;
×
451
        const auto id   = stc->MarkerAdd(line, m_marker_breakpoint.number());
×
452
        m_breakpoints[v[0]] = std::make_tuple(filename, id, line);
×
453
        return;
×
454
      }
455
    }
×
456
  }
457
  else if (MATCH(PATH) == 1)
×
458
  {
459
    if (path(v[0]).is_absolute())
×
460
    {
461
      log::trace("debug path") << v[0];
×
462

463
      if (m_path.string() == v[0] && m_process != nullptr)
×
464
      {
465
        // Debug same exe as before, so
466
        // reapply all breakpoints.
467
        for (const auto& it : m_breakpoints)
×
468
        {
469
          m_process->write(
×
470
            m_entry.break_set() + " " + std::get<0>(it.second).string() + ":" +
×
471
            std::to_string(std::get<2>(it.second) + 1));
×
472
        }
473
      }
474

475
      m_path = path(v[0]);
×
476
      m_frame->debug_exe(m_path);
×
477
    }
478

479
    m_stdout.clear();
×
480
  }
481
  else if (regex v(
×
482
             {{m_entry.regex_stdout(debug_entry::regex_t::AT_PATH_LINE)},
483
              {m_entry.regex_stdout(debug_entry::regex_t::AT_LINE)}});
×
484
           v.search(m_stdout) > 0)
×
485
  {
486
    if (v.size() == 2)
×
487
    {
488
      m_path                 = path(wex::path(m_path.parent_path()), v[0]);
×
489
      m_path_execution_point = m_path;
×
490
      log::trace("debug path and exec") << m_path.string();
×
491
    }
492
    data.indicator_no(data::stc::IND_DEBUG);
×
493
    data.control().line(std::stoi(v.back()));
×
494
    m_stdout.clear();
×
495
  }
496
  else if (regex v(
×
497
             {{m_entry.regex_stdout(debug_entry::regex_t::VARIABLE_MULTI)},
498
              {m_entry.regex_stdout(debug_entry::regex_t::VARIABLE)}});
×
499
           v.search(m_stdout) > 0)
×
500
  {
501
    m_stdout.clear();
×
502

503
    if (allow_open(m_path))
×
504
    {
505
      if (auto* stc = m_frame->open_file(m_path); stc != nullptr)
×
506
      {
507
        wxCommandEvent event(
508
          wxEVT_COMMAND_MENU_SELECTED,
509
          ID_EDIT_DEBUG_VARIABLE);
×
510
        event.SetString(v[0]);
×
511
        wxPostEvent(stc, event);
×
512
        return;
×
513
      }
×
514
    }
515
  }
516
  else if (MATCH(EXIT) >= 0)
×
517
  {
518
    is_finished();
×
519
    m_stdout.clear();
×
520
  }
521
  else if (clear_breakpoints(m_stdout))
×
522
  {
523
    m_stdout.clear();
×
524
  }
525
  else if (regex v("error: "); v.search(m_stdout) == 0)
×
526
  {
527
    m_stdout.clear();
×
528
    m_frame->pane_show("PROCESS");
×
529
  }
530
  else if (regex v("'(.*)'"); v.search(m_stdout) == 1)
×
531
  {
532
    if (wex::path filename(v[0]); allow_open(filename))
×
533
    {
534
      m_path = path(v[0]);
×
535
      log::trace("debug path") << v[0];
×
536
    }
×
537
    m_stdout.clear();
×
538
  }
539
  else if (!m_stdout.contains("{"))
×
540
  {
541
    m_stdout.clear();
×
542
  }
×
543

544
  if (data.control().line() > 0 && allow_open(m_path))
×
545
  {
546
    m_frame->open_file(m_path, data);
×
547
  }
548
}
×
549

550
void wex::debug::set_entry(const std::string& debugger)
47✔
551
{
552
  if (std::vector<wex::debug_entry> v; menus::load("debug", v))
141✔
553
  {
554
    if (debugger.empty())
8!
555
    {
556
      m_entry = v[0];
×
557
    }
558
    else if (const auto& it = std::ranges::find_if(
8✔
559
               v,
560
               [debugger](auto const& e)
8✔
561
               {
562
                 return e.name() == debugger;
8✔
563
               });
8✔
564
             it != v.end())
8!
565
    {
566
      m_entry = *it;
8✔
567
    }
568
    else
569
    {
570
      log("unknown debugger") << debugger;
×
571
      return;
×
572
    }
573

574
    m_frame->set_debug_entry(&m_entry);
8✔
575

576
    log::info("debug entries") << v.size() << "from" << menus::path().string()
24✔
577
                               << "debugger:" << m_entry.name();
8✔
578
  }
47!
579
}
580

581
bool wex::debug::show_dialog(wxWindow* parent)
×
582
{
583
  std::vector<std::string>      s;
×
584
  std::vector<wex::debug_entry> v;
×
585
  menus::load("debug", v);
×
586

587
  std::ranges::transform(
×
588
    v,
589
    std::back_inserter(s),
590
    [](const auto& i)
×
591
    {
592
      return i.name();
×
593
    });
594

595
  auto debugger = m_entry.name();
×
596
  if (!single_choice_dialog(
×
597
        data::window().parent(parent).title(_("Enter Debugger")),
×
598
        s,
599
        debugger))
600
  {
601
    return false;
×
602
  }
603

604
  config("debug.debugger").set(debugger);
×
605
  set_entry(debugger);
×
606
  return true;
×
607
}
×
608

609
bool wex::debug::toggle_breakpoint(int line, stc* stc)
3✔
610
{
611
  if (m_process == nullptr || stc == nullptr)
3✔
612
  {
613
    return false;
2✔
614
  }
615

616
  // If we already have a breakpoint, remove it.
617
  for (auto& it : m_breakpoints)
1!
618
  {
619
    if (line == std::get<2>(it.second) && std::get<0>(it.second) == stc->path())
×
620
    {
621
      stc->MarkerDeleteHandle(std::get<1>(it.second));
×
622
      m_breakpoints.erase(it.first);
×
623
      return m_process->write(m_entry.break_del() + " " + it.first);
×
624
    }
625
  }
626

627
  // Otherwise, set it.
628
  m_path = stc->path();
1✔
629

630
  log::trace("debug toggle breakpoint") << m_path;
1✔
631

632
  return m_process->write(
1✔
633
    m_entry.break_set() + " " + stc->path().string() + ":" +
2✔
634
    std::to_string(line + 1));
4✔
635
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc