runtime(tohtml): convert tohtml plugin to Vim9 script by mao-yining · Pull Request #19915 · vim/vim · GitHub
Skip to content

runtime(tohtml): convert tohtml plugin to Vim9 script#19915

Open
mao-yining wants to merge 22 commits intovim:masterfrom
mao-yining:tohtml
Open

runtime(tohtml): convert tohtml plugin to Vim9 script#19915
mao-yining wants to merge 22 commits intovim:masterfrom
mao-yining:tohtml

Conversation

@mao-yining
Copy link
Copy Markdown
Contributor

@mao-yining mao-yining commented Apr 4, 2026

I spent some time converting tohtml to vim9, initially to see if there would be any performance improvements.

But I'm not sure if it's an environment issue or something else — the converted version seems to perform worse than the original, and the progress bar during conversion doesn't show up either. So please do not merge for now.

I'd like to hear some feedback from everyone, and also have the tests run.

Thanks all!

@mao-yining
Copy link
Copy Markdown
Contributor Author

@Konfekt
Copy link
Copy Markdown
Contributor

Konfekt commented Apr 4, 2026

You could mark it as draft for now?

@mao-yining mao-yining marked this pull request as draft April 4, 2026 13:26
@mao-yining mao-yining force-pushed the tohtml branch 2 times, most recently from 8ee7d85 to 423896c Compare April 4, 2026 14:09
@chrisbra
Copy link
Copy Markdown
Member

chrisbra commented Apr 4, 2026

It used to be @fritzophrenic

@h-east
Copy link
Copy Markdown
Member

h-east commented Apr 5, 2026

@mao-yining
I profiled the converted script and found that the main line-processing loop (the while lnum <= end loop) runs as top-level script code, which is interpreted even in Vim9 script. Only code inside def functions is compiled to bytecode.

Wrapping the main loop in a def ProcessLines() function reduced execution time from ~37s to ~15s (2.5x speedup) on a 2000+ line file.

Changes:

  • Wrapped the main loop in def ProcessLines() and call it immediately after definition
  • Moved all_lines and start_lnum variables inside the function
  • Fixed Vim9 syntax issues required inside def:
    • Added space around : in ternary operator
    • Added space around - in lnum - 1
    • Changed tablist from list<string> to list<number> using mapnew() + str2nr(), since implicit string-to-number conversion is not allowed inside def

Below is a patch:
NOTE: I deliberately didn't change the indentation to minimize the differences. I would appreciate it if you could handle it that way.

diff --git a/runtime/syntax/2html.vim b/runtime/syntax/2html.vim
index 026406dc6..306ca9af0 100644
--- a/runtime/syntax/2html.vim
+++ b/runtime/syntax/2html.vim
@@ -1512,7 +1512,11 @@ if !settings.expand_tabs
   setlocal isprint+=9
 endif
 
-while lnum <= end
+def ProcessLines()
+  var all_lines = getline(lnum, end)
+  var start_lnum = lnum
+
+  while lnum <= end
 
   # If there are filler lines for diff mode, show these above the line.
   Add_diff_fill(lnum)
@@ -1535,7 +1539,7 @@ while lnum <= end
     endif
 
     # put numcol in a separate group for sake of unselectable text
-    new = (settings.number_lines ? HtmlFormat_n(numcol, FOLDED_ID, 0, lnum): "") .. HtmlFormat_t(new, FOLDED_ID, 0)
+    new = (settings.number_lines ? HtmlFormat_n(numcol, FOLDED_ID, 0, lnum) : "") .. HtmlFormat_t(new, FOLDED_ID, 0)
 
     # Skip to the end of the fold
     var new_lnum = foldclosedend(lnum)
@@ -1550,12 +1554,12 @@ while lnum <= end
     #
     # A line that is not folded, or doing dynamic folding.
     #
-    var line = getline(lnum)
+    var line = all_lines[lnum - start_lnum]
     var len = strlen(line)
 
     if settings.dynamic_folds
       # First insert a closing for any open folds that end on this line
-      while !empty(foldstack) && get(foldstack, 0).lastline == lnum-1
+      while !empty(foldstack) && get(foldstack, 0).lastline == lnum - 1
 	new ..= "</span></span>"
 	remove(foldstack, 0)
       endwhile
@@ -1710,9 +1714,9 @@ while lnum <= end
 	if settings.expand_tabs
 	  var offset = 0
 	  var idx = stridx(expandedtab, "\t")
-	  var tablist = split(&vts, ',')
+	  var tablist = mapnew(split(&vts, ','), (_, v) => str2nr(v))
 	  if empty(tablist)
-	    tablist = [ &ts ]
+	    tablist = [&ts]
 	  endif
 	  var tabidx = 0
 	  var tabwidth = 0
@@ -1789,6 +1793,8 @@ while lnum <= end
     pgb.Incr()
   endif
 endwhile
+enddef
+ProcessLines()
 
 # Diff filler is returned based on what needs inserting *before* the given line.
 # So to get diff filler at the end of the buffer, we need to use last line + 1

@mao-yining
Copy link
Copy Markdown
Contributor Author

@h-east Thanks for your suggestion. I've also optimized another similar place.

Does anyone have a better solution for this kind of code?

  let s:build_fun_lines[-1] =<< trim ENDLET
	blablabla
  ENDLET

My current solution is:

  trim_tmp =<< trim ENDLET
	blablabla
  ENDLET
  build_fun_lines += trim_tmp

@mao-yining mao-yining marked this pull request as ready for review April 5, 2026 14:45
@fritzophrenic
Copy link
Copy Markdown
Contributor

Thanks guys, this sounds very promising.

I haven't been active for a few years on this. I do have a limited test suite over here along with where I had been officially maintaining the script but I don't think I had any work in progress that isn't already included in the officially distributed runtime: https://sourceforge.net/p/vim-tohtml/code/ci/default/tree/

The tests aren't complete yet, I have a couple tests for very basic functionality (and checking the output is valid HTML) but mostly I had been adding tests for new features or bugfixes as I implemented them. Still, it's a lot better than nothing and we should make sure there are not any new test failures comparing the current version to the Vim9 script version.

Vim9 script is something I wanted to eventually include (see https://sourceforge.net/p/vim-tohtml/issues/28/ from years ago) but I never got around to experimenting with it even outside the plugin so once this MR is ready to go and is properly tested for regressions I think it should definitely merge.

@mao-yining
Copy link
Copy Markdown
Contributor Author

@fritzophrenic Seems this require java environment which I am not familiarize with. Could you please test it?

@fritzophrenic
Copy link
Copy Markdown
Contributor

Yeah I'll try to give that a try tonight or later in the week. I need to get set up again. It's using java only for running a standalone HTML validator as a test step, not to write or run the tests themselves.

@fritzophrenic
Copy link
Copy Markdown
Contributor

Hey I got the test running (and fixed a test issue caused by a change to diff highlighting sometime in the last few years). After downloading the changes for this pull request, I get several exceptions thrown during the test run, leading to test failures. I'll see if I can extract the exact option combinations leading to each error later, but for now here are the errors:

Caught exception in Test_line_numbers_with_filler(): Vim(eval):E1001: Variable not found: saved_style @ command line..script /home/ben/code/vim-src/src/testdir/runtest.vim[636]..function RunTheTest[63]..Test_line_numbers_with_filler[7]..tohtml#Convert2HTML[11]..script /home/ben/code/vim-tohtml/syntax/2html.vim[1799]..function <SNR>20_ProcessLines[7]..<SNR>20_Add_diff_fill[56]..<SNR>20_HtmlFormat_d[1]..<SNR>20_HtmlFormat[29]..<SNR>20_BuildStyleWrapper, line 30

The same error occurs in Test_whole_filler_disabled_one_file(), Test_whole_filler_disabled_two_files(), Test_whole_filler_disabled_uncopyable(), Test_whole_filler_enabled_one_file(), and Test_whole_filler_enabled_two_files(). So there's a good chance this issue is related to diff mode in some way.

Caught exception in Test_default_expandtab_from_foldcol(): Vim(eval):E1050: Colon required before a range: %foldclose! @ command line..script /home/ben/code/vim-src/src/testdir/runtest.vim[636]..function RunTheTest[63]..Test_default_expandtab_from_foldcol[6]..tohtml#Convert2HTML[11]..script /home/ben/code/vim-tohtml/syntax/2html.vim[1460]..function <SNR>20_ProcessFolds, line 36

This error occurs in many tests across several test files so I think it is related to building the dynamic fold column in general. Affected tests:

  • Test_default_expandtab_from_foldcol()
  • Test_default_expandtab_from_linenums()
  • Test_default_expandtab_from_setting()
  • Test_default_expandtab_from_tabstop()
  • Test_default_expandtab_from_vartabs()
  • Test_default_keep_tabs()
  • Test_expand_normal_tab()
  • Test_keep_vartab()
  • Test_direct_hi_rules()
  • Test_linked_hi_rules()
  • Test_all_variables_use_var()
  • Test_all_regions()
  • Test_diff_filler()
  • Test_fold_column_fallback()
  • Test_fold_column_generated_only()
  • Test_fold_column_input_only()
  • Test_fold_text()
  • Test_folding_disabled_fallback()
  • Test_folding_disabled_generated()
  • Test_folding_disabled_inputs()
  • Test_invalid()
  • Test_line_numbers()
  • Test_lnum_no_foldcolumn_fallback()
  • Test_lnum_no_foldcolumn_generated()
  • Test_lnum_no_foldcolumn_inputs()
  • Test_no_lnums_in_all()
  • Test_no_lnums_in_fallback()
  • Test_no_lnums_in_none()
  • Test_script_tag_highlander()
  • Test_script_tag_linenums()
  • Test_script_tag_omitted()
Caught exception in Test_script_version_nominal(): Vim(eval):E1001: Variable not found: lines @ command line..script /home/ben/code/vim-src/src/testdir/runtest.vim[636]..function RunTheTest[63]..Test_script_version_nominal[1]..script /home/ben/code/vim-tohtml/syntax/2html.vim[1799]..function <SNR>18_ProcessLines[7]..<SNR>18_Add_diff_fill, line 60

Also in test Test_script_version_piecemeal().

Caught exception in Test_nodoc_html_header_footer(): Vim(eval):E121: Undefined variable: win_list @ command line..script /home/ben/code/vim-src/src/testdir/runtest.vim[636]..function RunTheTest[63]..Test_nodoc_html_header_footer[13]..tohtml#Convert2HTML, line 15

Also impacts: Test_nodoc_links(), Test_nodoc_modeline_nodoc(), Test_script_tag_blank_linenums(), and Test_script_tag_dyn_folds().

I'll try to provide more info this weekend if I have time. If you want to run on your own, you just need to install a Java JRE and set up to build Vim from source so you're using the latest and greatest. I have the Vim repository checked out to ~/code/vim-src and my TOhtml repository checked out to ~/code/vim-tohtml. I downloaded the vnu.jar file to ~/code/vim-tohtml/testdir and installed OpenJDK JRE 25 from the Linux Mint software manager, then all it took was a "make" command to run the tests.

Hope that help, this looks like a really good start!

Comment thread runtime/plugin/tohtml.vim Outdated
@mao-yining
Copy link
Copy Markdown
Contributor Author

mao-yining commented Apr 11, 2026

@h-east
This will output <c2>: echo 'vert:│,fold:·,trunc:…'['vert:│,fold:·,trunc:…'->matchend('fold:')]
But vim9cmd echo 'vert:│,fold:·,trunc:…'['vert:│,fold:·,trunc:…'->matchend('fold:')] will output t.
Is this a bug or intentionally design?

What's more. All of these isn't what I expect. This is for extracting · in fold:·.

Comment thread runtime/syntax/2html.vim Outdated
Comment thread runtime/syntax/2html.vim Outdated
Comment thread runtime/syntax/2html.vim Outdated
Comment thread runtime/syntax/2html.vim Outdated
Comment thread runtime/syntax/2html.vim Outdated
Comment thread runtime/syntax/2html.vim Outdated
Comment thread runtime/syntax/2html.vim Outdated
@mao-yining
Copy link
Copy Markdown
Contributor Author

mao-yining commented Apr 11, 2026 via email

@h-east
Copy link
Copy Markdown
Member

h-east commented Apr 11, 2026

@h-east This will output <c2>: echo 'vert:│,fold:·,trunc:…'['vert:│,fold:·,trunc:…'->matchend('fold:')] But vim9cmd echo 'vert:│,fold:·,trunc:…'['vert:│,fold:·,trunc:…'->matchend('fold:')] will output t. Is this a bug or intentionally design?

What's more. All of these isn't what I expect. This is for extracting · in fold:·.

This is a specification of Vim9 script.
See ":help vim9-string-index"

vim9cmd echo 'vert:│,fold:·,trunc:…'[charidx('vert:│,fold:·,trunc:…', matchend('vert:│,fold:·,trunc:…', 'fold:'))]

@fritzophrenic
Copy link
Copy Markdown
Contributor

fritzophrenic commented Apr 11, 2026

Attaching log from the test run after the recent changes (cleaned up some for readability). The "lines" issue is still there, but we have resolved enough issues to get some interesting feedback from the HTML validator and other checks now as well. I probably won't have much time to dig into this over the weekend but can keep plinking away during the week.
test.log

@mao-yining
Copy link
Copy Markdown
Contributor Author

I don't have access to the internet during the week. I'll take another look next weekend. I also have no idea about the lines issue.

Comment thread runtime/syntax/2html.vim Outdated
Comment thread runtime/syntax/2html.vim
@fritzophrenic
Copy link
Copy Markdown
Contributor

After my last two fixes, there is only one remaining test failure:

From /home/ben/code/vim-tohtml/testdir/test_hi_link.vim:
Found errors in Test_direct_hi_rules():
command line..script /home/ben/code/vim-src/src/testdir/runtest.vim[636]..function RunTheTest[63]..Test_direct_hi_rules[4]..8_check_direct_styles[8]..8_check_style_rule line 3: Extra CSS rule for LineNr: Expected False but got 21

It looks like this failure comes from a duplicate LineNr style definition being added for some reason. Here's the options line from the generated HTML in case it's relevant:

<meta name="settings" content="number_lines,dynamic_folds,use_css,diff_one_file,expand_tabs,prevent_copy=,use_input_for_pc=none">

@mao-yining
Copy link
Copy Markdown
Contributor Author

From /home/ben/code/vim-tohtml/testdir/test_hi_link.vim:
Found errors in Test_direct_hi_rules():
command line..script /home/ben/code/vim-src/src/testdir/runtest.vim[636]..function RunTheTest[63]..Test_direct_hi_rules[4]..8_check_direct_styles[8]..8_check_style_rule line 3: Extra CSS rule for LineNr: Expected False but got 21

This failure appears to be caused by a duplicate LineNr style definition being added for some reason. Here's the options line from the generated HTML in case it's relevant:

<meta name="settings" content="number_lines,dynamic_folds,use_css,diff_one_file,expand_tabs,prevent_copy=,use_input_for_pc=none">

Maybe the issue is in this line:

call add(s:lines, '<meta name="settings" content="'..
	\ join(filter(keys(s:settings),'s:settings[v:val]'),',')..
	\ ',prevent_copy='..s:settings.prevent_copy..
	\ ',use_input_for_pc='..s:settings.use_input_for_pc..
	\ '"'..s:tag_close)

which I've converted to:

add(lines, '<meta name="settings" content="' ..
  join(filter(keys(settings), (_, k) => {
    var v = settings[k]
    if type(v) == v:t_number
      return v != 0
    elseif type(v) == v:t_bool
      return !empty(v)
    else
      return false
    endif
  }), ',') ..
  ',prevent_copy=' .. settings.prevent_copy ..
  ',use_input_for_pc=' .. settings.use_input_for_pc ..
  '"' .. tag_close)

But I haven't figured out what went wrong yet.

mao-yining and others added 21 commits April 17, 2026 19:59
Signed-off-by: Mao-Yining <mao.yining@outlook.com>
Co-authored-by: Hirohito Higashi <h.east.727@gmail.com>
Signed-off-by: Mao-Yining <mao.yining@outlook.com>
Signed-off-by: Mao-Yining <mao.yining@outlook.com>
- Change synIDattr attribute checks from non-empty string to explicit '1'
- Fix last_colors_name comparison (null → null_string)
- Fix inverse attribute logic in CSS generation
- Refactor ProgressBar.CalculateTicks into local function and fix pb_len <= 0 case
- Move lnum and end declarations to global scope for ProcessFolds()
- Remove duplicate local declarations
- Initialize lnum before optional g:html_start_line check
Co-Authored-By: fritzophrenic <1334420+fritzophrenic@users.noreply.github.com>
Co-Authored-By: fritzophrenic <1334420+fritzophrenic@users.noreply.github.com>
Co-authored-by: fritzophrenic <fritzophrenic@gmail.com>
@fritzophrenic
Copy link
Copy Markdown
Contributor

Oh, the meta line is fine as far as I know (I suppose I should check if I have a test for that). I was only providing the meta line so you could see what options are set by the test, to reproduce the issue on your end if you want to debug.

The actual issue is there are two duplicate CSS lines generated to define the LineNr highlight. Since I have a specific test for that, I assume there was a bugfix at some point to remove a duplicate LineNr highlight definition which seems to have regressed.

@fritzophrenic
Copy link
Copy Markdown
Contributor

fritzophrenic commented Apr 17, 2026

@chrisbra by the way, are you interested in adding the tohtml tests to the automated checks at all? I'm not familiar with how those are configured or anything but would be willing to learn and put together a pull request. They do depend on an external HTML validator tool, which currently is a vnu.jar java file needing a JRE installed, in case that affects the feasibility of adding as an automated test.

@mao-yining
Copy link
Copy Markdown
Contributor Author

I'm still considering the performance issue. It seems we've used too much string concatenation. Would it be better to use a list instead?

@chrisbra
Copy link
Copy Markdown
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants