diff --git a/README.md b/README.md index 9a16f73..ce4cd69 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ characters they both highlight. values, plot styles, plot-element modifiers, terminal names, built-in functions and variables, datablocks, macros, numbers with unit suffixes, and both string flavours. -- Sensible buffer-local editor settings (comments, `iskeyword`, - `matchpairs`), all undone by `b:undo_ftplugin`. +- Sensible buffer-local editor settings (comments, `iskeyword`), all undone + by `b:undo_ftplugin`. - No commands, no mappings, no global options. Nothing to configure. - Pure Vim script. @@ -110,8 +110,6 @@ The ftplugin sets a few buffer-local options: code does not auto-wrap. - `iskeyword+=_`, because gnuplot identifiers and its built-in variables (`GPVAL_TERM`, `STATS_mean_y`) contain underscores. -- `matchpairs+=<:>`, so `%` jumps between the brackets in - `set xrange [0:1]` style pairs. All of it is registered in `b:undo_ftplugin`, so `:setfiletype` away from gnuplot restores your defaults. diff --git a/doc/gnuplot.txt b/doc/gnuplot.txt index 5e486ea..89d8684 100644 --- a/doc/gnuplot.txt +++ b/doc/gnuplot.txt @@ -59,9 +59,6 @@ The ftplugin sets a few buffer-local options: 'iskeyword' gains `_`, because gnuplot identifiers and its built-in variables (`GPVAL_TERM`, `STATS_mean_y`) contain underscores. - 'matchpairs' gains `<:>`, so |%| jumps between the brackets in - `set xrange [0:1]` style pairs. - Everything is registered in |b:undo_ftplugin|, so leaving the filetype restores your defaults. diff --git a/ftplugin/gnuplot.vim b/ftplugin/gnuplot.vim index f1ddfde..309cb64 100644 --- a/ftplugin/gnuplot.vim +++ b/ftplugin/gnuplot.vim @@ -21,11 +21,8 @@ setlocal formatoptions+=croql " contain underscores. setlocal iskeyword+=_ -" `set xrange [0:1]` and friends: treat brackets as matched pairs. -setlocal matchpairs+=<:> - let b:undo_ftplugin = 'setlocal commentstring< comments< formatoptions<' - \ . ' iskeyword< matchpairs<' + \ . ' iskeyword<' let &cpoptions = s:cpo_save unlet s:cpo_save diff --git a/syntax/gnuplot.vim b/syntax/gnuplot.vim index 09e55d7..fa8813b 100644 --- a/syntax/gnuplot.vim +++ b/syntax/gnuplot.vim @@ -26,20 +26,27 @@ set cpoptions&vim syn iskeyword @,48-57,_ " Comments ------------------------------------------------------------------ -" A trailing backslash continues a comment onto the next line. -syn match gnuplotComment "#.*\%(\\\n.*\)*$" contains=gnuplotTodo,@Spell +" A trailing backslash continues a comment onto the next line, so the comment +" ends only at an end of line that is not preceded by one. +syn region gnuplotComment start="#" end="\\\@" -syn match gnuplotNumber "\<\.\d\+\%([eE][-+]\?\d\+\)\?\%(cm\|inch\|in\|mm\|pt\|pc\|bp\|dd\|cc\|pi\)\?\>" +" `in` or the longer spelling never matches. A trailing dot (`1.`) belongs to +" the number, and gnuplot reads a dot before a digit as a number start (`.5`) +" even right after an identifier. +syn match gnuplotNumber "\<\d\+\%(\.\d*\)\?\%([eE][-+]\?\d\+\)\?\%(cm\|inch\|in\|mm\|pt\|pc\|bp\|dd\|cc\|pi\)\?\w\@!" +syn match gnuplotNumber "\.\d\+\%([eE][-+]\?\d\+\)\?\%(cm\|inch\|in\|mm\|pt\|pc\|bp\|dd\|cc\|pi\)\?\>" syn match gnuplotNumber "\<0[xX]\x\+\>" " Built-in variables -------------------------------------------------------- @@ -75,18 +84,19 @@ syn match gnuplotBuiltinVar "\<\w\+_\%(records\|headers\|outofrange\|invalid\| syn keyword gnuplotBuiltinVar GNUTERM VoxelDistance GridDistance " Functions ----------------------------------------------------------------- -" Any identifier immediately followed by '(' reads as a call; the built-in list +" Any identifier followed by '(' reads as a call; the built-in list " is defined afterwards so it wins on the same match position. -syn match gnuplotFunction "\<\h\w*\ze(" -syn match gnuplotBuiltinFunction "\<\%(abs\|acosh\|acos\|airy\|arg\|asinh\|asin\|atan2\|atanh\|atan\|cbrt\|ceil\|conj\|cosh\|cos\|exp\|floor\|imag\|int\|log10\|log\|norm\|rand\|real\|round\|sgn\|sinh\|sin\|sqrt\|tanh\|tan\)\ze(" -syn match gnuplotBuiltinFunction "\<\%(bes[ijy][01n]\|Ai\|Bi\|Bessel[HIJKY]1\?2\?\|Elliptic[EKP]i\?\|erfc\|erfi\|erf\|expint\|gamma\|ibeta\|igamma\|inverf\|invibeta\|invigamma\|invnorm\|LambertW\|lgamma\|lnGamma\|SynchrotronF\|uigamma\|voigt\|zeta\)\ze(" -syn match gnuplotBuiltinFunction "\<\%(cdawson\|cerf\|faddeeva\|Fresnel[CS]\|VP\%(_fwhm\)\?\)\ze(" -syn match gnuplotBuiltinFunction "\<\%(gprintf\|sprintf\|strlen\|strstrt\|substr\|split\|join\|trim\|word\|words\|system\)\ze(" -syn match gnuplotBuiltinFunction "\<\%(column\|columnhead\%(er\)\?\|stringcolumn\|strcol\|exists\|valid\|value\|hsv2rgb\|palette\|rgbcolor\|voxel\|index\)\ze(" -syn match gnuplotBuiltinFunction "\<\%(time\%(column\)\?\|strftime\|strptime\|tm_\w\+\|weekdate_\%(iso\|cdc\)\)\ze(" +syn match gnuplotFunction "\<\h\w*\ze\s*(" +syn match gnuplotBuiltinFunction "\<\%(abs\|acosh\|acos\|airy\|arg\|asinh\|asin\|atan2\|atanh\|atan\|cbrt\|ceil\|conj\|cosh\|cos\|exp\|floor\|imag\|int\|log10\|log\|norm\|rand\|real\|round\|sgn\|sinh\|sin\|sqrt\|tanh\|tan\)\ze\s*(" +syn match gnuplotBuiltinFunction "\<\%(bes[ijy][01n]\|Ai\|Bi\|Bessel[HIJKY]1\?2\?\|Elliptic[EKP]i\?\|erfc\|erfi\|erf\|expint\|gamma\|ibeta\|igamma\|inverf\|invibeta\|invigamma\|invnorm\|LambertW\|lgamma\|lnGamma\|SynchrotronF\|uigamma\|voigt\|zeta\)\ze\s*(" +syn match gnuplotBuiltinFunction "\<\%(cdawson\|cerf\|faddeeva\|Fresnel[CS]\|VP\%(_fwhm\)\?\)\ze\s*(" +syn match gnuplotBuiltinFunction "\<\%(gprintf\|sprintf\|strlen\|strstrt\|substr\|split\|join\|trim\|word\|words\|system\)\ze\s*(" +syn match gnuplotBuiltinFunction "\<\%(column\|columnhead\%(er\)\?\|stringcolumn\|strcol\|exists\|valid\|value\|hsv2rgb\|palette\|rgbcolor\|voxel\|index\)\ze\s*(" +syn match gnuplotBuiltinFunction "\<\%(time\%(column\)\?\|strftime\|strptime\|tm_\w\+\|weekdate_\%(iso\|cdc\)\)\ze\s*(" " Operators ----------------------------------------------------------------- -syn match gnuplotOperator "[-+*/%^!~?:.,]" +" A dot before a digit starts a number, not a concatenation. +syn match gnuplotOperator "[-+*/%^!~?:,]\|\.\d\@!" syn match gnuplotOperator "\*\*" syn match gnuplotOperator "[=!<>]=" syn match gnuplotOperator "[<>]" @@ -364,6 +374,11 @@ syn keyword gnuplotConditional if else syn keyword gnuplotRepeat do while for in syn keyword gnuplotCommand array sum +" Synchronization ----------------------------------------------------------- +" Datablocks have no length limit, so no fixed lookback can tell whether a line +" sits inside one. Parse from the top of the file instead. +syn sync fromstart + " Highlight links ----------------------------------------------------------- " The group names come from the grammar's capture taxonomy, where an alias " names the clause family a word belongs to rather than the word itself: `cmd` diff --git a/tests/run.vim b/tests/run.vim index 56190a3..d1f5d96 100644 --- a/tests/run.vim +++ b/tests/run.vim @@ -13,6 +13,9 @@ filetype plugin on syntax enable let v:errors = [] +" A script error (bad expression, unknown function) skips its assertion +" without touching v:errors, and nvim still exits 0, so it is caught below. +let v:errmsg = '' " Raw syntax group at (line, col). Deliberately NOT synIDtrans: Vim collapses " Conditional, Repeat and Keyword all into Statement, which would make most of @@ -35,6 +38,22 @@ function! s:Word(text, word, expected) abort \ printf('%s -> %s', a:text, a:word)) endfunction +" Assert the group under the first occurrence of {word} on line {lnum} of a +" multi-line buffer. Queried bottom-up, so the answer depends on the file +" syncing from the top rather than on what was parsed before. +function! s:Line(lines, lnum, word, expected) abort + enew! + setlocal filetype=gnuplot + call setline(1, a:lines) + let l:col = stridx(a:lines[a:lnum - 1], a:word) + 1 + if l:col <= 0 + call add(v:errors, printf('fixture error: %s not on line %d', a:word, a:lnum)) + return + endif + call assert_equal(a:expected, s:Group(a:lnum, l:col), + \ printf('%s -> line %d %s', join(a:lines, ' | '), a:lnum, a:word)) +endfunction + " --- commands --------------------------------------------------------------- call s:Word('plot sin(x)', 'plot', 'gnuplotCommand') call s:Word('p sin(x)', 'p', 'gnuplotCommand') @@ -92,6 +111,17 @@ call s:Word('set size 1inch, 2mm', '1inch', 'gnuplotNumber') " datablock contents are inert call s:Word('$d << EOD', '$d', 'gnuplotDatablockName') +" A deep line reads as data even when nothing above it was parsed first. +call s:Line(['$d << EOD'] + repeat(['plot 1 # x'], 200) + ['EOD'], 150, + \ 'plot', 'gnuplotDatablock') +" gnuplot ends the block on the first line starting with the terminator in +" column one: ` EOD` is data, `EODxyz` closes the block. +call s:Line(['$d << EOD', ' EOD', 'plot 1'], 2, 'EOD', 'gnuplotDatablock') +call s:Line(['$d << EOD', ' EOD', 'plot 1'], 3, 'plot', 'gnuplotDatablock') +call s:Line(['$d << EOD', '1 2', 'EODxyz', 'plot 1'], 4, 'plot', 'gnuplotCommand') +" The header may follow another command on the same line. +call s:Line(['print 1; $d << EOD', '1 2', 'EOD'], 1, '$d', 'gnuplotDatablockName') +call s:Line(['print 1; $d << EOD', '1 2', 'EOD'], 2, '1', 'gnuplotDatablock') call s:Word('set key noautotitle', 'noautotitle', 'gnuplotOption') " --- tiers ------------------------------------------------------------------ @@ -105,7 +135,24 @@ call s:Word('set border front', 'front', 'gnuplotFlag') call s:Word('plot "data.dat"', '"data.dat"', 'gnuplotString') call s:Word("plot 'data.dat'", "'data.dat'", 'gnuplotString') call s:Word('# a comment', '# a comment', 'gnuplotComment') +" A trailing backslash continues the comment; without one the next line is code. +call s:Line(['# comment\', 'print 42'], 2, 'print', 'gnuplotComment') +call s:Line(['print 1 # c\', 'print 42'], 2, '42', 'gnuplotComment') +call s:Line(['# comment', 'print 42'], 2, 'print', 'gnuplotCommand') +" gnuplot closes an unterminated string at the end of the line, unless a +" trailing backslash continues it. +call s:Line(['print "unfinished', 'print 42'], 2, 'print', 'gnuplotCommand') +call s:Line(["print 'unfinished", 'print 42'], 2, 'print', 'gnuplotCommand') +call s:Line(['print "abc\', 'def"', 'print 4'], 2, 'def', 'gnuplotString') +call s:Line(['print "abc\', 'def"', 'print 4'], 3, 'print', 'gnuplotCommand') +call s:Line(['print ''abc\', 'def''', 'print 4'], 2, 'def', 'gnuplotString') +call s:Word('print "a\"b" . x', 'x', '') call s:Word('a = 42', '42', 'gnuplotNumber') +" Leading and trailing dots are part of the number, not the `.` operator. +call s:Word('a = .5', '.', 'gnuplotNumber') +call s:Word('a = 1.', '.', 'gnuplotNumber') +call s:Word('a = 1.e3', 'e', 'gnuplotNumber') +call s:Word('a = "x" . b', '.', 'gnuplotOperator') call s:Word('set size 10cm, 5cm', '10cm', 'gnuplotNumber') call s:Word('a = pi', 'pi', 'gnuplotConstant') call s:Word('load @macro', '@macro', 'gnuplotMacro') @@ -115,12 +162,22 @@ call s:Word('print STATS_mean_y', 'STATS_mean_y', 'gnuplotBuiltinVar') " --- functions -------------------------------------------------------------- call s:Word('plot sin(x)', 'sin', 'gnuplotBuiltinFunction') call s:Word('a = myfunc(2)', 'myfunc', 'gnuplotFunction') +" gnuplot allows whitespace before the argument list. +call s:Word('print sin (0)', 'sin', 'gnuplotBuiltinFunction') +call s:Word('a = myfunc (2)', 'myfunc', 'gnuplotFunction') " --- control flow ----------------------------------------------------------- call s:Word('if (a > 1) { print "y" }', 'if', 'gnuplotConditional') call s:Word('do for [i=1:10] { print i }', 'do', 'gnuplotRepeat') call s:Word('do for [i=1:10] { print i }', 'for', 'gnuplotRepeat') +" --- ftplugin --------------------------------------------------------------- +" `<` and `>` are comparisons in gnuplot, never delimiters, so `%` must not +" pair them. +enew! +setlocal filetype=gnuplot +call assert_equal(-1, stridx(&l:matchpairs, '<:>'), 'matchpairs has no <:>') + " --- filetype detection ----------------------------------------------------- " `.gp` is not claimed: it belongs to PARI/GP in the stock runtimes. for s:name in ['t.plt', 't.plot', 't.gnu', 't.gpi', 't.gnuplot', @@ -150,6 +207,10 @@ function! s:Report(lines) abort endif endfunction +if !empty(v:errmsg) + call add(v:errors, 'script error: ' . v:errmsg) +endif + if empty(v:errors) call s:Report(['ok - all syntax assertions passed']) qall!