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

tstack / lnav / 25348852825-3024

04 May 2026 11:18PM UTC coverage: 69.963% (+0.7%) from 69.226%
25348852825-3024

push

github

tstack
[ui] horizontal scroll should work on columns

Related to #1685

7 of 141 new or added lines in 5 files covered. (4.96%)

7760 existing lines in 84 files now uncovered.

57014 of 81492 relevant lines covered (69.96%)

622491.44 hits per line

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

0.0
/src/external_editor.cc
1
/**
2
 * Copyright (c) 2024, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <filesystem>
31
#include <optional>
32
#include <thread>
33
#include <vector>
34

35
#include "external_editor.hh"
36

37
#include <fcntl.h>
38
#include <stdlib.h>
39
#include <unistd.h>
40

41
#include "base/auto_fd.hh"
42
#include "base/auto_pid.hh"
43
#include "base/fs_util.hh"
44
#include "base/injector.hh"
45
#include "base/lnav_log.hh"
46
#include "base/paths.hh"
47
#include "base/result.h"
48
#include "base/time_util.hh"
49
#include "external_editor.cfg.hh"
50
#include "fmt/format.h"
51

52
namespace lnav::external_editor {
53

54
static time64_t
55
get_config_dir_mtime(const std::filesystem::path& path,
×
56
                     const std::filesystem::path& config_dir)
57
{
58
    static const auto HOME_PATH = lnav::paths::userhome();
59

60
    if (config_dir.empty()) {
×
61
        return 0;
×
62
    }
63

64
    auto parent = path.parent_path();
×
65
    std::error_code ec;
×
66

67
    while (!parent.empty()) {
×
68
        if (parent == HOME_PATH) {
×
69
            break;
×
70
        }
71
        auto config_path = parent / config_dir;
×
72
        log_trace("  checking editor config dir: %s", config_path.c_str());
×
73
        auto mtime = std::filesystem::last_write_time(config_path, ec);
×
74
        if (!ec) {
×
75
            time64_t retval = mtime.time_since_epoch().count();
×
76

77
            log_debug("    found editor config dir: %s (%lld)",
×
78
                      config_path.c_str(),
79
                      retval);
80
            return retval;
×
81
        }
82

83
        for (const auto& sib : std::filesystem::directory_iterator(parent, ec))
×
84
        {
85
            if (!sib.is_directory()) {
×
UNCOV
86
                continue;
×
87
            }
88

89
            auto sib_config_path = sib.path() / config_dir;
×
90
            auto mtime = std::filesystem::last_write_time(config_path, ec);
×
91
            if (!ec) {
×
UNCOV
92
                time64_t retval = mtime.time_since_epoch().count();
×
93

UNCOV
94
                log_debug("    found editor config dir: %s (%lld)",
×
95
                          config_path.c_str(),
96
                          retval);
UNCOV
97
                return retval;
×
98
            }
99
        }
100

101
        auto new_parent = parent.parent_path();
×
102
        if (new_parent == parent) {
×
UNCOV
103
            return 0;
×
104
        }
UNCOV
105
        parent = new_parent;
×
106
    }
UNCOV
107
    return 0;
×
108
}
109

110
static std::optional<std::string>
UNCOV
111
get_impl(const std::filesystem::path& path)
×
112
{
113
    const auto& cfg = injector::get<const config&>();
×
UNCOV
114
    std::vector<std::tuple<time64_t, bool, bool, const impl*>> candidates;
×
115

116
    log_debug("editor impl count: %zu", cfg.c_impls.size());
×
117
    for (const auto& [name, impl] : cfg.c_impls) {
×
118
        const auto full_cmd = fmt::format(FMT_STRING("{} > /dev/null 2>&1"),
×
UNCOV
119
                                          impl.i_test_command);
×
120

UNCOV
121
        log_debug(" testing editor impl %s using: %s",
×
122
                  name.c_str(),
123
                  full_cmd.c_str());
124
        if (system(full_cmd.c_str()) == 0) {
×
125
            log_info("  detected editor: %s", name.c_str());
×
126
            auto prefers = impl.i_prefers.pp_value
×
127
                ? impl.i_prefers.pp_value->find_in(path.string())
×
128
                      .ignore_error()
×
129
                      .has_value()
×
130
                : false;
×
131
            auto disfavors = impl.i_disfavors.pp_value
×
UNCOV
132
                ? impl.i_disfavors.pp_value->find_in(path.string())
×
UNCOV
133
                      .ignore_error()
×
UNCOV
134
                      .has_value()
×
135
                : false;
×
136
            auto config_dir_mtime
137
                = get_config_dir_mtime(path, impl.i_config_dir);
×
138
            log_info("    config-dir-mtime: %llu, prefers: %s, disfavors: %s",
×
139
                     config_dir_mtime,
140
                     prefers ? "yes" : "no",
141
                     disfavors ? "yes" : "no");
UNCOV
142
            candidates.emplace_back(
×
UNCOV
143
                config_dir_mtime, prefers, disfavors, &impl);
×
144
        }
145
    }
146

UNCOV
147
    std::sort(candidates.begin(),
×
148
              candidates.end(),
UNCOV
149
              [](const auto& lhs, const auto& rhs) {
×
UNCOV
150
                  const auto& [lmtime, lprefers, ldisfavors, limpl] = lhs;
×
UNCOV
151
                  const auto& [rmtime, rprefers, rdisfavors, rimpl] = rhs;
×
152

UNCOV
153
                  if (lmtime > rmtime) {
×
154
                      return true;
×
155
                  }
156

157
                  if (lmtime < rmtime) {
×
UNCOV
158
                      return false;
×
159
                  }
160

UNCOV
161
                  if (lprefers && !rprefers) {
×
162
                      return true;
×
163
                  }
164

165
                  if (!lprefers && rprefers) {
×
166
                      return false;
×
167
                  }
168

UNCOV
169
                  if (ldisfavors && !rdisfavors) {
×
170
                      return false;
×
171
                  }
172

UNCOV
173
                  if (!ldisfavors && rdisfavors) {
×
174
                      return true;
×
175
                  }
176

177
                  return limpl->i_command < rimpl->i_command;
×
178
              });
179

180
    if (candidates.empty()) {
×
181
        return std::nullopt;
×
182
    }
183

UNCOV
184
    return std::get<3>(candidates.front())->i_command;
×
185
}
186

187
Result<void, std::string>
188
open(std::filesystem::path p, uint32_t line, uint32_t col)
×
189
{
190
    const auto impl = get_impl(p);
×
191

192
    if (!impl) {
×
UNCOV
193
        const static std::string MSG = "no external editor found";
×
194

195
        return Err(MSG);
×
196
    }
197

UNCOV
198
    log_info("external editor command: %s", impl->c_str());
×
199

200
    auto err_pipe = TRY(auto_pipe::for_child_fd(STDERR_FILENO));
×
UNCOV
201
    auto child_pid_res = lnav::pid::from_fork();
×
UNCOV
202
    if (child_pid_res.isErr()) {
×
203
        return Err(child_pid_res.unwrapErr());
×
204
    }
205

206
    auto child_pid = child_pid_res.unwrap();
×
207
    err_pipe.after_fork(child_pid.in());
×
208
    if (child_pid.in_child()) {
×
209
        auto open_res
210
            = lnav::filesystem::open_file("/dev/null", O_RDONLY | O_CLOEXEC);
×
UNCOV
211
        open_res.then([](auto_fd&& fd) {
×
212
            fd.copy_to(STDIN_FILENO);
×
213
            fd.copy_to(STDOUT_FILENO);
×
214
        });
×
UNCOV
215
        setenv("FILE_PATH", p.c_str(), 1);
×
216
        auto line_str = fmt::to_string(line);
×
UNCOV
217
        setenv("LINE", line_str.c_str(), 1);
×
UNCOV
218
        auto col_str = fmt::to_string(col);
×
219
        setenv("COL", col_str.c_str(), 1);
×
220

UNCOV
221
        execlp("sh", "sh", "-c", impl->c_str(), nullptr);
×
UNCOV
222
        _exit(EXIT_FAILURE);
×
UNCOV
223
    }
×
UNCOV
224
    log_debug("started external editor, pid: %d", child_pid.in());
×
225

UNCOV
226
    std::string error_queue;
×
227
    std::thread err_reader(
UNCOV
228
        [err = std::move(err_pipe.read_end()), &error_queue]() mutable {
×
229
            while (true) {
230
                char buffer[1024];
UNCOV
231
                auto rc = read(err.get(), buffer, sizeof(buffer));
×
UNCOV
232
                if (rc <= 0) {
×
UNCOV
233
                    break;
×
234
                }
235

UNCOV
236
                error_queue.append(buffer, rc);
×
237
            }
238

UNCOV
239
            log_debug("external editor stderr closed");
×
UNCOV
240
        });
×
241

UNCOV
242
    auto finished_child = std::move(child_pid).wait_for_child();
×
UNCOV
243
    err_reader.join();
×
UNCOV
244
    if (!finished_child.was_normal_exit()) {
×
UNCOV
245
        return Err(fmt::format(FMT_STRING("editor failed with signal {}"),
×
UNCOV
246
                               finished_child.term_signal()));
×
247
    }
UNCOV
248
    auto exit_status = finished_child.exit_status();
×
UNCOV
249
    if (exit_status != 0) {
×
UNCOV
250
        return Err(fmt::format(FMT_STRING("editor failed with status {} -- {}"),
×
251
                               exit_status,
UNCOV
252
                               error_queue));
×
253
    }
254

UNCOV
255
    return Ok();
×
256
}
257

258
}  // namespace lnav::external_editor
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