Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ jobs:
- name: Run tests – Lookup-file (failed + restart)
run: make test-lookup-file || echo "fail" > lookup-file.status

- name: Run tests – Rerun (editor commands + on-demand convergence)
run: make test-rerun || echo "fail" > rerun.status

- name: Report summary
if: always()
run: |
Expand All @@ -116,6 +119,7 @@ jobs:
[ ! -f stream.status ] && pass=$((pass+1)) || fail=$((fail+1))
[ ! -f register.status ] && pass=$((pass+1)) || fail=$((fail+1))
[ ! -f lookup-file.status ] && pass=$((pass+1)) || fail=$((fail+1))
[ ! -f rerun.status ] && pass=$((pass+1)) || fail=$((fail+1))
echo "Passed: $pass" >> "$GITHUB_STEP_SUMMARY"
echo "Failed: $fail" >> "$GITHUB_STEP_SUMMARY"
[ $fail -eq 0 ]
Expand Down
14 changes: 14 additions & 0 deletions EDITOR-PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ prime the VFS via `(register)` and `(open)`, then send `(resume)` to begin
compilation — avoiding restarts from missing files. In `-stream` mode the
engine starts paused, so `(resume)` is required to begin compilation.

```scheme
(rerun t)
(rerun nil)
(rerun-once)
```

Control convergence-on-idle. When enabled with `(rerun t)`, TeXpresso waits
for a short idle window after each change; if the `.aux` file changed since
the last pass, the engine is respawned with the prior-pass aux to converge
TOC/refs. `(rerun nil)` disables it. `(rerun-once)` triggers a single
immediate pass regardless of the enabled state — useful for committing the
current aux on demand before switching to reading mode. Defaults to disabled
at startup.

```scheme
(synctex-forward "path" line)
```
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,11 @@ test-register:
test-lookup-file:
bash test/test-lookup-file.sh

test-rerun:
bash test/test-rerun.sh

macos-app: texpresso
@[ "$$(uname)" = "Darwin" ] || { echo "macos-app requires macOS"; exit 1; }
bash scripts/build-macos-app.sh

.PHONY: all dev clean config texpresso common texpresso-xetex re2c compile_commands.json fill-tectonic-cache test-texlive test-tectonic test-texpresso test-stream test-open-base64 test-register test-lookup-file macos-app
.PHONY: all dev clean config texpresso common texpresso-xetex re2c compile_commands.json fill-tectonic-cache test-texlive test-tectonic test-texpresso test-stream test-open-base64 test-register test-lookup-file test-rerun macos-app
5 changes: 5 additions & 0 deletions src/frontend/driver.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ static void usage(void)

int main(int argc, const char **argv)
{
// Unbuffered stderr so debug/[fatal] lines hit disk before a crash.
setvbuf(stderr, NULL, _IONBF, 0);

char work_dir[PATH_MAX];

if (!getcwd(work_dir, PATH_MAX))
Expand Down Expand Up @@ -351,6 +354,8 @@ int main(int argc, const char **argv)
.stream_mode = stream_mode,
// In stream mode, start paused: editor primes the VFS before (resume).
.paused = stream_mode,
.rerun_enabled = false,
.rerun_once_pending = false,
};

int exit_code = 0;
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ struct persistent_state {

bool line_output, use_tectonic, use_texlive, initialize_only, stream_mode;
bool paused;
bool rerun_enabled;
bool rerun_once_pending;
};

bool texpresso_main(struct persistent_state *ps);
Expand Down
15 changes: 15 additions & 0 deletions src/frontend/editor.c
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,21 @@ bool editor_parse(fz_context *ctx,
goto arity;
*out = (struct editor_command){.tag = EDIT_RESUME, .resume = {}};
}
else if (strcmp(verb, "rerun") == 0)
{
if (len != 2)
goto arity;
bool status =
truth_value(ctx, stack, val_array_get(ctx, stack, command, 1));
*out = (struct editor_command){.tag = EDIT_RERUN,
.rerun = {.status = status}};
}
else if (strcmp(verb, "rerun-once") == 0)
{
if (len != 1)
goto arity;
*out = (struct editor_command){.tag = EDIT_RERUN_ONCE, .rerun_once = {}};
}
else
{
fprintf(stderr, "[command] unknown verb: %s\n", verb);
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ enum EDITOR_COMMAND
EDIT_REGISTER,
EDIT_PAUSE,
EDIT_RESUME,
EDIT_RERUN,
EDIT_RERUN_ONCE,
};

struct editor_change
Expand Down Expand Up @@ -127,6 +129,13 @@ struct editor_command

struct {
} resume;

struct {
bool status;
} rerun;

struct {
} rerun_once;
};
};

Expand Down
13 changes: 13 additions & 0 deletions src/frontend/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ struct txp_engine_class
synctex_t *(*synctex)(txp_engine *self, fz_buffer **buf);
fileentry_t *(*find_file)(txp_engine *self, fz_context *ctx, const char *path);
void (*notify_file_changes)(txp_engine *self, fz_context *ctx, fileentry_t *entry, int offset);
bool (*aux_dirty)(txp_engine *self);
bool (*is_finishing)(txp_engine *self);
void (*start_finishing)(txp_engine *self);
void (*finish_convergence)(txp_engine *self, fz_context *ctx);
};

#define TXP_ENGINE_DEF_CLASS \
Expand All @@ -93,6 +97,11 @@ struct txp_engine_class
const char *path); \
static void engine_notify_file_changes(txp_engine *self, fz_context *ctx, \
fileentry_t *entry, int offset); \
static bool engine_aux_dirty(txp_engine *_self); \
static bool engine_is_finishing(txp_engine *_self); \
static void engine_start_finishing(txp_engine *_self); \
static void engine_finish_convergence(txp_engine *_self, \
fz_context *ctx); \
\
static struct txp_engine_class _class = { \
.destroy = engine_destroy, \
Expand All @@ -107,6 +116,10 @@ struct txp_engine_class
.detect_changes = engine_detect_changes, \
.end_changes = engine_end_changes, \
.notify_file_changes = engine_notify_file_changes, \
.aux_dirty = engine_aux_dirty, \
.is_finishing = engine_is_finishing, \
.start_finishing = engine_start_finishing, \
.finish_convergence = engine_finish_convergence, \
}

#endif // GENERIC_ENGINE_H_
18 changes: 18 additions & 0 deletions src/frontend/engine_dvi.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ static void engine_notify_file_changes(txp_engine *_self,
{
}

static bool engine_aux_dirty(txp_engine *_self)
{
return false;
}

static bool engine_is_finishing(txp_engine *_self)
{
return false;
}

static void engine_start_finishing(txp_engine *_self)
{
}

static void engine_finish_convergence(txp_engine *_self, fz_context *ctx)
{
}

txp_engine *txp_create_dvi_engine(fz_context *ctx, const char *dvi_path, dvi_reshooks hooks)
{
fz_buffer *buffer = fz_read_file(ctx, dvi_path);
Expand Down
18 changes: 18 additions & 0 deletions src/frontend/engine_pdf.c
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,24 @@ static void engine_notify_file_changes(txp_engine *_self,
{
}

static bool engine_aux_dirty(txp_engine *_self)
{
return false;
}

static bool engine_is_finishing(txp_engine *_self)
{
return false;
}

static void engine_start_finishing(txp_engine *_self)
{
}

static void engine_finish_convergence(txp_engine *_self, fz_context *ctx)
{
}

txp_engine *txp_create_pdf_engine(fz_context *ctx, const char *pdf_path)
{
fz_document *doc = fz_open_document(ctx, pdf_path);
Expand Down
154 changes: 154 additions & 0 deletions src/frontend/engine_tex.c
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ struct tex_engine
query_t query;
char path[1024];
} deferred;

bool aux_dirty;
bool finishing;
};

// Backtrackable process state & VFS representation
Expand Down Expand Up @@ -236,6 +239,22 @@ static void pop_process(fz_context *ctx, struct tex_engine *self)
log_rollback(ctx, self->log, mark);
}

static void clear_convergence_stash(fz_context *ctx, struct tex_engine *self)
{
fileentry_t *e;
for (int index = 0; (e = filesystem_scan(self->fs, &index));)
{
if (e->edit_data_from_convergence && e->edit_data)
{
fz_drop_buffer(ctx, e->edit_data);
e->edit_data = NULL;
e->edit_data_from_convergence = false;
}
}
self->aux_dirty = false;
self->finishing = false;
}

static bool read_query(struct tex_engine *self, channel_t *t, query_t *q)
{
process_t *p = get_process(self);
Expand Down Expand Up @@ -799,6 +818,8 @@ static void answer_query(fz_context *ctx, struct tex_engine *self, query_t *q)
editor_append(BUF_LOG, output_data(e), pos);
else if (self->st.stdout.entry == e)
editor_append(BUF_OUT, output_data(e), pos);
else
self->aux_dirty = true;
a.tag = A_DONE;
channel_write_answer(self->c, p->fd, &a);
break;
Expand Down Expand Up @@ -965,6 +986,8 @@ static void revert_trace(trace_entry_t *te)
static void rollback_processes(fz_context *ctx, struct tex_engine *self, int reverted, int trace)
{
self->deferred.active = false;
clear_convergence_stash(ctx, self);

fprintf(
stderr,
"rolling back to position %d\nbefore rollback: %d bytes of output\n",
Expand Down Expand Up @@ -1394,6 +1417,137 @@ static void engine_notify_file_changes(txp_engine *_self,
rollback_add_change(ctx, self, entry, offset);
}

static bool is_system_output(const char *path)
{
if (strcmp(path, "stdout") == 0)
return true;
const char *dot = strrchr(path, '.');
if (!dot)
return false;
return strcmp(dot, ".log") == 0
|| strcmp(dot, ".xdv") == 0
|| strcmp(dot, ".dvi") == 0
|| strcmp(dot, ".pdf") == 0
|| strcmp(dot, ".synctex") == 0;
}

// Reset per-entry `seen` counters before respawning the engine. Without
// this, the new engine's Q_OPRD never enters the `e->seen < 0` branch
// in answer_query, so no first-open trace entry is generated. Later
// rollbacks would walk back through the trace without ever bringing
// entry->seen below the changed offset, walking past trace[0] into OOB
// memory and crashing in revert_trace.
static void reset_seen_for_respawn(struct tex_engine *self)
{
fileentry_t *e;
for (int index = 0; (e = filesystem_scan(self->fs, &index));)
e->seen = -1;
}

static bool engine_aux_dirty(txp_engine *_self)
{
SELF;
return self->aux_dirty;
}

static bool engine_is_finishing(txp_engine *_self)
{
SELF;
return self->finishing;
}

// Mark the engine as needing to run to end-of-document. The main loop
// keeps calling engine_step (because need_advance honours is_finishing),
// so the engine processes its remaining queries between SDL events —
// UI stays responsive. When the engine closes its socket, status flips
// to DOC_TERMINATED and finish_convergence does the post-pass work.
static void engine_start_finishing(txp_engine *_self)
{
SELF;
self->finishing = true;
}

static void engine_finish_convergence(txp_engine *_self, fz_context *ctx)
{
SELF;
if (!self->finishing)
return;
if (engine_get_status(_self) != DOC_TERMINATED)
return;

self->finishing = false;

bool converged = true;
fileentry_t *e;
for (int index = 0; (e = filesystem_scan(self->fs, &index));)
{
if (e->saved.level != FILE_WRITE || !e->saved.data)
continue;
if (is_system_output(e->path))
continue;
bool stashed_match = e->edit_data && e->edit_data_from_convergence
&& e->saved.data->len == e->edit_data->len
&& memcmp(e->saved.data->data, e->edit_data->data,
e->saved.data->len) == 0;
if (!stashed_match)
{
converged = false;
break;
}
}

if (converged)
{
fprintf(stderr, "[rerun] aux byte-stable, convergence reached\n");
// Finishing left the latest process dead (fd=-1) at the top of the
// snapshot stack. Strip dead processes so get_process() returns a
// live snapshot the next user edit can roll back into.
while (self->process_count > 0 && get_process(self)->fd == -1)
pop_process(ctx, self);
if (self->process_count == 0)
{
reset_seen_for_respawn(self);
incdvi_reset(self->dvi);
synctex_rollback(ctx, self->stex, 0);
editor_truncate(BUF_OUT, output_data(self->st.stdout.entry));
editor_truncate(BUF_LOG, output_data(self->st.log.entry));
prepare_process(ctx, self);
}
self->aux_dirty = false;
return;
}

for (int index = 0; (e = filesystem_scan(self->fs, &index));)
{
if (e->saved.level != FILE_WRITE || !e->saved.data)
continue;
if (is_system_output(e->path))
continue;
// Don't clobber edit_data the editor pushed (interpret_open sets
// edit_data without the convergence flag).
if (e->edit_data && !e->edit_data_from_convergence)
continue;
if (e->edit_data)
fz_drop_buffer(ctx, e->edit_data);
// Copy, don't share: log_rollback truncates saved.data->len in place.
e->edit_data = fz_new_buffer_from_copied_data(ctx, e->saved.data->data,
e->saved.data->len);
e->edit_data_from_convergence = true;
}

while (self->process_count > 0)
pop_process(ctx, self);

reset_seen_for_respawn(self);
incdvi_reset(self->dvi);
synctex_rollback(ctx, self->stex, 0);
editor_truncate(BUF_OUT, output_data(self->st.stdout.entry));
editor_truncate(BUF_LOG, output_data(self->st.log.entry));

prepare_process(ctx, self);
self->aux_dirty = false;
}

static void engine_begin_changes(txp_engine *_self, fz_context *ctx)
{
SELF;
Expand Down
Loading
Loading