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

tstack / lnav / 17657281416-2508

11 Sep 2025 09:05PM UTC coverage: 64.984% (-0.2%) from 65.166%
17657281416-2508

push

github

tstack
[log2src] show source vars in message details

Improve selection of external editors and preserve
cursor location when opening from prompt.

Add CLion and RustRover as external editors.

Add breakpoint support

262 of 629 new or added lines in 26 files covered. (41.65%)

4 existing lines in 3 files now uncovered.

45653 of 70253 relevant lines covered (64.98%)

404292.82 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/result.h"
47
#include "base/time_util.hh"
48
#include "external_editor.cfg.hh"
49
#include "fmt/format.h"
50
#include "shlex.hh"
51

52
namespace lnav::external_editor {
53

54
static time64_t
NEW
55
get_config_dir_mtime(const std::filesystem::path& path,
×
56
                     const std::filesystem::path& config_dir)
57
{
NEW
58
    if (config_dir.empty()) {
×
NEW
59
        return 0;
×
60
    }
61

NEW
62
    auto parent = path.parent_path();
×
NEW
63
    std::error_code ec;
×
64

NEW
65
    while (!parent.empty()) {
×
NEW
66
        auto config_path = parent / config_dir;
×
NEW
67
        auto mtime = std::filesystem::last_write_time(config_path, ec);
×
NEW
68
        if (!ec) {
×
NEW
69
            auto retval = mtime.time_since_epoch().count();
×
70

NEW
71
            log_debug("  found editor config dir: %s (%lld)",
×
72
                      config_path.c_str(),
73
                      retval);
NEW
74
            return retval;
×
75
        }
NEW
76
        auto new_parent = parent.parent_path();
×
NEW
77
        if (new_parent == parent) {
×
NEW
78
            return 0;
×
79
        }
NEW
80
        parent = new_parent;
×
81
    }
NEW
82
    return 0;
×
83
}
84

85
static std::optional<impl>
NEW
86
get_impl(const std::filesystem::path& path)
×
87
{
88
    const auto& cfg = injector::get<const config&>();
×
NEW
89
    std::vector<std::tuple<time64_t, bool, impl>> candidates;
×
90

91
    log_debug("editor impl count: %d", cfg.c_impls.size());
×
NEW
92
    for (const auto& [name, impl] : cfg.c_impls) {
×
93
        const auto full_cmd = fmt::format(FMT_STRING("{} > /dev/null 2>&1"),
×
NEW
94
                                          impl.i_test_command);
×
95

NEW
96
        log_debug(" testing editor impl %s using: %s",
×
97
                  name.c_str(),
98
                  full_cmd.c_str());
99
        if (system(full_cmd.c_str()) == 0) {
×
NEW
100
            log_info("  detected editor: %s", name.c_str());
×
NEW
101
            auto prefers = impl.i_prefers.pp_value
×
NEW
102
                ? impl.i_prefers.pp_value->find_in(path.string())
×
NEW
103
                      .ignore_error()
×
NEW
104
                      .has_value()
×
NEW
105
                : false;
×
NEW
106
            candidates.emplace_back(
×
NEW
107
                get_config_dir_mtime(path, impl.i_config_dir), prefers, impl);
×
108
        }
109
    }
110

NEW
111
    std::stable_sort(candidates.begin(),
×
112
                     candidates.end(),
NEW
113
                     [](const auto& lhs, const auto& rhs) {
×
NEW
114
                         const auto& [lmtime, lprefers, limpl] = lhs;
×
NEW
115
                         const auto& [rmtime, rprefers, rimpl] = rhs;
×
116

NEW
117
                         return lmtime > rmtime ||
×
NEW
118
                             (lmtime == rmtime && lprefers);
×
119
                     });
120

NEW
121
    if (candidates.empty()) {
×
NEW
122
        return std::nullopt;
×
123
    }
124

NEW
125
    return std::get<2>(candidates.front());
×
126
}
127

128
Result<void, std::string>
NEW
129
open(std::filesystem::path p, uint32_t line, uint32_t col)
×
130
{
NEW
131
    const auto impl = get_impl(p);
×
132

NEW
133
    if (!impl) {
×
134
        const static std::string MSG = "no external editor found";
×
135

136
        return Err(MSG);
×
137
    }
138

NEW
139
    log_info("external editor command: %s", impl->i_command.c_str());
×
140

141
    auto err_pipe = TRY(auto_pipe::for_child_fd(STDERR_FILENO));
×
142
    auto child_pid_res = lnav::pid::from_fork();
×
143
    if (child_pid_res.isErr()) {
×
144
        return Err(child_pid_res.unwrapErr());
×
145
    }
146

147
    auto child_pid = child_pid_res.unwrap();
×
148
    err_pipe.after_fork(child_pid.in());
×
149
    if (child_pid.in_child()) {
×
150
        auto open_res
151
            = lnav::filesystem::open_file("/dev/null", O_RDONLY | O_CLOEXEC);
×
152
        open_res.then([](auto_fd&& fd) {
×
153
            fd.copy_to(STDIN_FILENO);
×
154
            fd.copy_to(STDOUT_FILENO);
×
155
        });
×
156
        setenv("FILE_PATH", p.c_str(), 1);
×
NEW
157
        auto line_str = fmt::to_string(line);
×
NEW
158
        setenv("LINE", line_str.c_str(), 1);
×
NEW
159
        auto col_str = fmt::to_string(col);
×
NEW
160
        setenv("COL", col_str.c_str(), 1);
×
161

NEW
162
        execlp("sh", "sh", "-c", impl->i_command.c_str(), nullptr);
×
163
        _exit(EXIT_FAILURE);
×
164
    }
×
165
    log_debug("started external editor, pid: %d", child_pid.in());
×
166

167
    std::string error_queue;
×
168
    std::thread err_reader(
169
        [err = std::move(err_pipe.read_end()), &error_queue]() mutable {
×
170
            while (true) {
171
                char buffer[1024];
172
                auto rc = read(err.get(), buffer, sizeof(buffer));
×
173
                if (rc <= 0) {
×
174
                    break;
×
175
                }
176

177
                error_queue.append(buffer, rc);
×
178
            }
179

180
            log_debug("external editor stderr closed");
×
181
        });
×
182

183
    auto finished_child = std::move(child_pid).wait_for_child();
×
184
    err_reader.join();
×
185
    if (!finished_child.was_normal_exit()) {
×
186
        return Err(fmt::format(FMT_STRING("editor failed with signal {}"),
×
187
                               finished_child.term_signal()));
×
188
    }
189
    auto exit_status = finished_child.exit_status();
×
190
    if (exit_status != 0) {
×
191
        return Err(fmt::format(FMT_STRING("editor failed with status {} -- {}"),
×
192
                               exit_status,
193
                               error_queue));
×
194
    }
195

196
    return Ok();
×
197
}
198

199
}  // 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