Skip to content
Open
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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,10 @@ jobs:
v ${{ matrix.custom && '-d ui2_custom_rendering' || '' }} test ui2/ui/ui_scroll_immediate_test.v
v ${{ matrix.custom && '-d ui2_custom_rendering' || '' }} test ui2/examples/scrollview

# Covers the generated companion program and, on a V with `-new-compiler`,
# a full new-compiler build of the IDE itself.
- name: Test IDE
run: v ${{ matrix.custom && '-d ui2_custom_rendering' || '' }} test ui2/ide

- name: Build all examples
run: v run ui2/examples/build_examples.vsh ${{ matrix.custom && '-d ui2_custom_rendering' || '' }}
45 changes: 43 additions & 2 deletions ide/model.v
Original file line number Diff line number Diff line change
Expand Up @@ -980,9 +980,50 @@ fn (mut app IdeApp) save_document() !string {
return path
}

// companion_main_template uses a raw string with @PLACEHOLDER@ tokens instead of
// interpolation, because the generated source itself contains `${...}` and the
// new compiler mis-parses escaped `\$` inside an interpolated literal. The
// user-supplied names are emitted as double-quoted literals because vml_escape
// escapes `"` but not `'`.
const companion_main_template = r'module main

import ui2

const @CONST@ = $embed_file("@VML@").to_string()

fn build_form() ui2.Element {
return ui2.element_from_vml(@CONST@, ui2.bounds()) or {
eprintln("Could not load @VML@: ${err}")
ui2.screen(0x@BG@, [])
}
}

fn handle_form_event(event string) {
println("event: ${event}")
}

fn main() {
ui2.run_window("@NAME@", @W@, @H@, build_form, handle_form_event)
}
'

fn companion_main_source(app &IdeApp, vml_name string) string {
const_name := 'form_source'
return "module main\n\nimport ui2\n\nconst ${const_name} = \$embed_file('${vml_escape(vml_name)}').to_string()\n\nfn build_form() ui2.Element {\n\treturn ui2.element_from_vml(${const_name}, ui2.bounds()) or {\n\t\teprintln('Could not load ${vml_escape(vml_name)}: \${err}')\n\t\tui2.screen(0x${app.form_background:06x}, [])\n\t}\n}\n\nfn handle_form_event(event string) {\n\tprintln('event: \${event}')\n}\n\nfn main() {\n\tui2.run_window('${vml_escape(app.form_name)}', ${int(app.form_width)}, ${int(app.form_height)}, build_form, handle_form_event)\n}\n"
// replace_each substitutes in a single pass, so a name containing another
// placeholder cannot be expanded a second time.
return companion_main_template.replace_each([

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: add regression coverage for the template's escaping and single-pass replacement

The existing test_saved_form_and_generated_main_compile_together only generates the default Form1 / form.vml case and checks the companion with the default compiler. Please extend coverage with an apostrophe-containing filename such as Mo's Form.vml and a placeholder-like filename such as @BG@.vml; quoted/backslash replacement values can be checked in a source-generation unit test without creating platform-invalid filenames. Assert that $embed_file, ${err}, and ${event} survive literally in the generated source, and that inserted @BG@ text is not substituted a second time. An automated -new-compiler build of the IDE would also catch the original host-compilation regression, which the current default-compiler companion check alone cannot cover.

@struckchure struckchure Sep 22, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in b4190a6:

  • test_companion_main_source_escapes_names_and_replaces_placeholders_once: form name Mo's "Form" \ @W@ and file name @BG@.vml; asserts the run_window literal escapes " and \ while passing ' through, that $embed_file("@BG@.vml"), ${err} and ${event} survive literally, that the @BG@/@W@ text coming in through user values is not expanded again, and that no placeholder token remains.
  • test_saved_form_and_generated_main_compile_together now generates and -checks companions for form.vml, Mo's Form.vml and @BG@.vml.
  • test_ide_builds_with_the_new_compiler: full -new-compiler build of ./ide (-check stops before the C stage where the original failure showed up). It skips with a note on a V that doesn't know the flag, which is the case for the 0.5.2 release binaries CI pins. Verified locally that it fails on the parent commit and passes here.

CI now runs v test ui2/ide on every matrix entry. One extra data point: on current V master the new compiler is the default (with a fallback to the cached 0.5.2), so there a plain v test ide against the old template already fails in the C stage with the undeclared err/event.

'@CONST@',
'form_source',
'@VML@',
vml_escape(vml_name),
'@BG@',
'${app.form_background:06x}',
'@NAME@',
vml_escape(app.form_name),
'@W@',
int(app.form_width).str(),
'@H@',
int(app.form_height).str(),
])
}

fn (mut app IdeApp) generate_companion(overwrite bool) !string {
Expand Down
50 changes: 48 additions & 2 deletions ide/model_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -241,19 +241,65 @@ fn test_source_mode_uses_monospace_and_toolbar_uses_icons() {
}
}

fn test_saved_form_and_generated_main_compile_together() {
fn test_companion_main_source_escapes_names_and_replaces_placeholders_once() {
mut app := new_ide_app('.')
app.form_name = 'Mo\'s "Form" \\ @W@'
app.form_background = 0x123abc
source := companion_main_source(app, '@BG@.vml')
// The user-supplied names are double-quoted literals, so `'` passes through
// while `"` and `\` are escaped.
assert source.contains('ui2.run_window("Mo\'s \\"Form\\" \\\\ @W@", 760, 520, build_form, handle_form_event)'), source
// replace_each is single-pass: a placeholder token that arrives through a
// user value lands in the output verbatim instead of being expanded again.
assert source.contains(r'const form_source = $embed_file("@BG@.vml").to_string()'), source
assert source.contains('ui2.screen(0x123abc, [])'), source
// The generated program's own interpolations must survive as source text.
assert source.contains(r'eprintln("Could not load @BG@.vml: ${err}")'), source
assert source.contains(r'println("event: ${event}")'), source
for token in ['@CONST@', '@VML@', '@NAME@', '@H@'] {
assert !source.contains(token), source
}
}

fn check_generated_companion(vml_name string) {
test_dir := os.join_path(@VMODROOT, 'ide', '.generated_test_${os.getpid()}')
os.mkdir_all(test_dir) or { panic(err) }
defer {
os.rmdir_all(test_dir) or {}
}
mut app := new_ide_app(test_dir)
app.path_input = os.join_path(test_dir, 'form.vml')
app.form_name = vml_name.all_before_last('.vml')
app.path_input = os.join_path(test_dir, vml_name)
app.add_component('button', 40, 48)
app.components[0].event_handler = 'button_clicked'
app.sync_source()
app.save_document() or { panic(err) }
main_path := app.generate_companion(false) or { panic(err) }
result := os.execute('${os.quoted_path(@VEXE)} -check ${os.quoted_path(main_path)}')
assert result.exit_code == 0, '${vml_name}: ${result.output}'
}

fn test_saved_form_and_generated_main_compile_together() {
// The names cover an apostrophe, a space, and a placeholder-like token, all of
// which have to reach the generated `main.v` as valid string literals.
check_generated_companion('form.vml')
check_generated_companion("Mo's Form.vml")
check_generated_companion('@BG@.vml')
}

fn test_ide_builds_with_the_new_compiler() {
// The original regression only surfaced in the C stage of `-new-compiler`,
// which emitted the template's escaped `${err}` / `${event}` as live
// interpolations. `-check` stops before that stage, so this needs a full build.
out_path := os.join_path(os.temp_dir(), 'ui2_ide_new_compiler_${os.getpid()}')
defer {
os.rm(out_path) or {}
}
cmd := '${os.quoted_path(@VEXE)} -new-compiler -o ${os.quoted_path(out_path)} ${os.quoted_path(os.join_path(@VMODROOT, 'ide'))}'
result := os.execute(cmd)
if result.output.contains('Unknown argument `-new-compiler`') {
eprintln('skipping: this V does not support -new-compiler')
return
}
assert result.exit_code == 0, result.output
}