Skip to content

Enable JupyterLab syntax highlighting, fix tab completion and continuation-prompt hang - #58

Open
mottelet wants to merge 12 commits into
Calysto:masterfrom
mottelet:fix-jupyterlab-syntax-highlighting
Open

mottelet wants to merge 12 commits into
Calysto:masterfrom
mottelet:fix-jupyterlab-syntax-highlighting

Conversation

@mottelet

@mottelet mottelet commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the lack of syntax highlighting reported in this Jupyter Discourse thread: Scilab cells showed as plain text in the live JupyterLab editor, even though PDF/HTML export via nbconvert was already highlighted (that path uses Pygments, matched on language_info['name'] == 'scilab', a real Pygments lexer, so it was unaffected).

JupyterLab has no built-in Scilab CodeMirror mode. jupyterlab-scilab is now published to provide one — a real CodeMirror language for Scilab, with correct // comments (not %, which is what Octave/Matlab-derived modes use) and ~2200 keywords/builtins sourced directly from Scilab's own getscilabkeywords()/what(). Source: https://github.com/mottelet/jupyterlab-scilab.

This PR:

  • Sets codemirror_mode: "scilab" / mimetype: "text/x-scilab" in language_info and kernel.json so JupyterLab picks up jupyterlab-scilab's registered language. Without it installed, cells fall back to plain text in the live editor (as before this PR) — nbconvert/Pygments export is unaffected either way.
  • Adds jupyterlab-scilab as an optional dependency: pip install scilab-kernel[jupyterlab]. Not a hard dependency, since scilab_kernel is also used outside JupyterLab (classic Notebook, other frontends), where pulling in the full jupyterlab package (a transitive dependency of jupyterlab-scilab) would be unwanted bloat.
  • Fixes tab completion: it ran completion("obj") directly and parsed Scilab's own console echo, which changed format at least ~2 years ago (now quotes string-array output), breaking every completion candidate. Fixed by using printf("%s\n", completion(...)) instead, which sidesteps Scilab's own display formatting; guarded against the empty-match case (printf errors on an empty array) without ever assigning to a variable that could shadow one of the user's own, since completion runs in the same persistent Scilab session as their code. Also fixed a latent bug the change exposed: Scilab's terminal control codes glued to the front of the first line of output could leak into a genuine single-match result.
  • Completion on an empty prefix (cursor right after a non-identifier character like +, -, or inside plot(1:10,) now returns the full registered-name list, matching IPython's own convention, rather than either the old broken quoted dump or an artificially suppressed empty list.
  • Fixes a ~30 second hang when a cell leaves a block unclosed (function x=f(y) with no endfunction, an if/for/while/... with no matching end). metakernel's own recovery for a stuck continuation prompt sends Ctrl-C, but scilab-adv-cli doesn't respond to SIGINT while mid-block, so that recovery always burned its full 30s timeout before showing a generic "Timed out" error. Fixed with a small REPLWrapper subclass that recovers via a bare end instead — closes any block type in well under a second, with nested unclosed blocks getting one end per level.
  • Cleans up 39 pre-existing ruff findings that were blocking CI (confirmed unrelated to any of the above by cloning master fresh and running ruff there directly — it already failed with the same errors).

Test plan

  • Verified locally against JupyterLab 4.6: with jupyterlab-scilab installed, a fresh notebook with the Scilab kernel highlights keywords/strings/numbers/comments correctly, including previously-tricky cases (// comments, transpose ' after ]/), primitives like real/imag/cvode/plot2d2).
  • Confirmed via a direct kernel_info_reply query that the running kernel reports codemirror_mode: "scilab", and via a faithful reproduction of JupyterLab 4.6's getMimeTypeByLanguage()/findBest()/findByName() matching code that this resolves correctly against jupyterlab-scilab's registered language.
  • Tab completion verified against a live kernel: multi-match, single-match, no-match, and empty-prefix cases all return correct results; confirmed via who_user() that nothing is left behind in the Scilab session.
  • Continuation-prompt fix verified against a live kernel: unclosed function/if/for (including nested) now report a clear error in ~0.2-0.3s instead of hanging ~30s; normal complete multi-line blocks are unaffected; the session stays fully functional afterward.
  • All fixes verified together end-to-end in one session.
  • ruff check . and py_compile clean on all changed files.

🤖 Generated with Claude Code

mottelet and others added 4 commits September 23, 2026 11:24
Scilab cells showed as plain text in the live JupyterLab editor even
though PDF/HTML export was already highlighted (nbconvert/Pygments
picks language_info['name'] == 'scilab', a real Pygments lexer, so
that path was unaffected).

JupyterLab has no CodeMirror language for "scilab", and picks a mode
by matching language_info against its registry. Reporting
codemirror_mode: "octave" (Scilab's syntax being close to
Octave/Matlab) makes it resolve to the closest available mode.

Note that mimetype alone is not enough: JupyterLab 4.6's
getMimeTypeByLanguage() builds a fallback object as
{mimetype, name, ext}, but the language registry's findBest()
destructures {mime, name, extensions} -- a key-name mismatch that
silently drops the mimetype-only path back to plaintext. Setting
codemirror_mode explicitly goes through the working code path
(findByName(), case-insensitive) instead.

Known limitation: Octave's CodeMirror mode only recognizes "%" as a
line comment, while Scilab uses "//", so "//" comments aren't styled
and JupyterLab's Toggle Comment (Ctrl+/) inserts "%" -- invalid
Scilab syntax. A full fix needs a dedicated Scilab CodeMirror
language (a companion JupyterLab extension); this gets every other
token (strings, numbers, operators, keywords) highlighted correctly
in the meantime.

Fixes: https://discourse.jupyter.org/t/syntax-highlighting-scilab-language/37910

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jupyterlab-scilab (https://pypi.org/project/jupyterlab-scilab/) is now
published: a real CodeMirror language for Scilab (correct "//"
comments, ~2200 keywords/builtins sourced from Scilab's own
getscilabkeywords()/what()), so the Octave-mode stopgap from this PR
is no longer needed -- and is now actively harmful: an explicit
codemirror_mode always wins over JupyterLab's own name-match fallback,
so keeping "octave" here would keep forcing Octave-style highlighting
even once jupyterlab-scilab is installed.

Switched language_info/kernel.json back to codemirror_mode: "scilab" /
mimetype: "text/x-scilab", and added jupyterlab-scilab as an optional
dependency (`pip install scilab-kernel[jupyterlab]`) rather than a
hard one, since scilab_kernel is also used outside JupyterLab (classic
Notebook, other frontends) where pulling in the full jupyterlab
package would be unwanted bloat.

Verified: a live kernel now reports codemirror_mode: "scilab" in its
kernel_info_reply, and this resolves correctly to text/x-scilab
against jupyterlab-scilab's registered language (confirmed both by a
faithful reproduction of JupyterLab 4.6's actual
getMimeTypeByLanguage()/findBest()/findByName() matching code, and by
a live JupyterLab instance with jupyterlab-scilab installed from PyPI).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Not caused by this PR -- confirmed by cloning master fresh and running
ruff there directly: it already fails with the same 39 errors. CI runs
ruff over the whole repo rather than just the diff, so PR Calysto#58 inherited
an already-broken check. Fixing it here since that's what's actually
blocking this PR's CI.

- I001/UP008/UP010/UP021/W605: mechanical (ruff --fix), reviewed --
  import sorting, super() without arguments, dropped the unneeded
  py2 __future__ import, text=True instead of universal_newlines=True,
  and raw-string prefixes for the two genuinely-intended backslash
  literals.
- UP031 (percent formatting): converted to f-strings throughout.
- PLW1510: subprocess.run() for the macOS bundle-detection mdfind call
  now passes check=False explicitly, preserving the existing behavior
  (a non-zero/empty result is already handled by the `len(bundles) > 0`
  check right after).
- TRY201: `raise e` -> `raise` in extract_figures' re-raise path (e is
  still used in the error_handler() branch, so the `as e` binding
  stays; only the bare re-raise changes, preserving the original
  traceback).
- BLE001/S110 in _handle_svg: narrowed from bare `except Exception:
  pass` to `except (ValueError, ExpatError)`, since minidom parsing
  and the int()/unpacking calls right after are the only things that
  realistically fail here (GnuPlot producing an SVG shape the code
  doesn't expect), logged via self.log.debug instead of silently
  swallowed.
- BLE001 in check.py: kept as a blind `except Exception`, with a
  justifying noqa -- it's a standalone diagnostic script whose whole
  purpose is to report *any* connection failure instead of crashing
  with a raw traceback.
- RUF012: annotated the three intentionally-shared class-level dicts/
  lists (language_info, and jupyter_kernel_test's own
  code_display_data/completion_samples convention) as typing.ClassVar.

Verified: `ruff check .` passes clean, all files still py_compile, and
a live kernel smoke-tested end to end (disp, %plot producing a real
PNG display_data through the edited figure_size/axes_size f-strings,
correct language_info).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tab completion ran completion("obj") directly and parsed Scilab's own
console echo of the result. That echo used to be an unquoted, bracketed
format Scilab dropped at least ~2 years ago in favor of displaying
string arrays quoted (`"a"  "b"  ...`), which the parsing here was
never updated for -- so every completion candidate now comes back
wrapped in literal double quotes.

Fixed by using printf("%s\n", completion(...)) instead, which prints
each match bare, one per line, sidestepping Scilab's own display
formatting entirely (and the quoting behind whatever future formatting
changes might come). Two things needed care:

- printf errors out on an empty array, so it's now only called when
  completion() isn't empty (checked without assigning the result to a
  variable -- get_completions() runs in the same persistent Scilab
  session as the user's own code, so nothing here should risk shadowing
  a variable of theirs).
- Scilab's terminal control codes end up glued to the front of the
  *first* line of any console output. With the old quoted/bracketed
  format this only ever landed on a line that got filtered out by
  coincidence; with printf's plain output it could leak into a real
  match (reproduced with a single-match completion, e.g. "getscilabkeyword"
  -> "\x1b[4l \x08\x1b[0mgetscilabkeywords" instead of
  "getscilabkeywords"). Now stripped via a small regex before parsing.

Verified against a live kernel: multi-match, single-match, and
no-match completions all return clean results, and `who_user()`
confirms nothing gets left behind in the session.

Also includes the same pre-existing ruff cleanup as PR Calysto#58 (this
branch was cut from master, which still has it) -- see that PR's
commit message for the itemized list; same fixes, same reasoning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ghlighting

# Conflicts:
#	scilab_kernel/kernel.py
@mottelet mottelet changed the title Enable JupyterLab syntax highlighting via Octave CodeMirror mode Enable JupyterLab syntax highlighting via jupyterlab-scilab, fix tab completion Sep 24, 2026
Running a cell that leaves a block unclosed -- function x=f(y) with no
endfunction, an if/for/while/select/try with no matching end -- left
Scilab sitting at its continuation prompt waiting for the rest of the
block, and the kernel just hung. metakernel's REPLWrapper does detect
this (fast, well under a second) and has a built-in recovery: send
Ctrl-C, wait for a normal prompt, then raise a clean, catchable
ValueError. But scilab-adv-cli does not respond to SIGINT while
waiting for more input, so that recovery wait always ran out its full
budget (pexpect's default of 30s) before giving up -- so every
incomplete cell cost a good 30 seconds before showing a generic
"Timed out" error, though the session did eventually recover on its
own (confirmed: the next cell executes normally afterward).

Fixed with a small REPLWrapper subclass (_ScilabREPLWrapper) that
overrides just the continuation-prompt recovery step. A bare "end"
reliably closes any Scilab block type (function/if/for/while/...) and
returns to the top-level prompt in well under a second -- verified
directly against a live Scilab process for each block type. Nested
unclosed blocks (e.g. an unclosed if containing an unclosed for) need
one "end" per level, so it's sent repeatedly (capped at 50 attempts)
until a normal prompt reappears, matching what a user closing the
blocks by hand would have to type anyway.

Everything else about metakernel's flow is unchanged -- the "soft
continuation" empty-line retry, the eventual ValueError, and its
message ("Continuation prompt found - input was incomplete:\n<code>")
are all metakernel's own, already well-suited to surfacing a clear
error to the user; this only fixes the part that was actually broken
for Scilab specifically.

Verified against a live kernel: unclosed function/if/for now each
report the error in ~0.2-0.3s (down from 30s+), a nested unclosed
if+for needs and gets two "end"s and also recovers in ~0.3s, normal
complete multi-line blocks (if/end, function/endfunction, for/end)
are unaffected and execute correctly, and the session remains fully
functional for subsequent cells in every case.

Also includes the same pre-existing ruff cleanup as Calysto#58/Calysto#59 (this
branch was cut from master, which still has that debt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hting

# Conflicts:
#	scilab_kernel/kernel.py
@mottelet mottelet changed the title Enable JupyterLab syntax highlighting via jupyterlab-scilab, fix tab completion Enable JupyterLab syntax highlighting via jupyterlab-scilab, fix tab completion and continuation-prompt hang Sep 24, 2026
mottelet and others added 3 commits September 24, 2026 11:32
metakernel's do_complete() calls get_completions() unconditionally,
even when the cursor sits right after a non-identifier character
(e.g. "+", "-") and there is no partial word to complete -- info['obj']
is "" in that case. get_completions() didn't guard against that, so it
ran completion(""), which matches every name Scilab knows (thousands),
and the substring filter `if info['obj'] in line` let all of them
through too (every string contains "" as a substring) -- a huge,
useless completion menu on a bare Tab after an operator.

Verified against a live kernel: "+" and "-" now return zero matches;
normal completions ("plo", "sin", single/no-match cases) are
unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
metakernel's parser sets cursor_start to 0 (start of the whole buffer)
whenever there is no partial identifier at the cursor -- regardless of
whether any completions were actually found. After "plot(1:10,", that
gives cursor_start=0, cursor_end=10 with zero matches from Scilab.
With no matches that wide range should be irrelevant to a kernel-only
completer, but JupyterLab also runs a generic word-from-document
completer alongside the kernel one, and hands it a range that happens
to span real words ("plot", "1", "10") already on the line -- so it
offers them as if they were completions, even though nothing was
actually being completed.

Fixed by collapsing cursor_start to cursor_end (a zero-width point at
the cursor) whenever get_completions() finds nothing, so no wide
buffer range is ever reported for JupyterLab's other completer to act
on. Normal completions are unaffected -- their cursor_start still
reflects the real start of the partial word being completed.

Verified against a live kernel: "plot(1:10," and "plot(" both now
report matches=0 with cursor_start == cursor_end (previously
cursor_start=0); "+"/"-" unaffected (already 0 matches, now also
correctly zero-width); "plo"/"sin" completions unaffected (still
return their normal candidate list with the correct word-start range).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverts the "return [] when info['obj'] is empty" guard added a
couple of commits back. That guard was meant to fix a real problem
(4199 raw, quoted-looking matches dumped after typing e.g. "+"), but
suppressing the kernel's own completions entirely on an empty prefix
isn't actually the right fix for that, and isn't how Jupyter kernels
conventionally behave: IPython itself shows the full registered-name
list when completing on an empty prefix, and scilab-adv-cli's own
interactive completion does the same. That original "huge menu"
complaint is better understood as being about the quoting/parsing
bug fixed earlier in this same function, not about the match count.

Confirmed there is no way to selectively suppress this for Scilab:
JupyterLab's completion providers (kernel, context, ...) are always
all queried and their results unconditionally merged (checked
directly in @jupyterlab/completer's ProviderReconciliator) -- there is
no per-language opt-out, and no rank-based short-circuiting. So the
right shape for scilab_kernel's own contribution is simply to match
the established, expected convention, same as every other kernel.

Verified against a live kernel: "+", "-", and "plot(1:10," now all
return the full ~4199-entry name list with a correct zero-width
cursor range (cursor_start == cursor_end, from metakernel's own
existing correction once matches is non-empty); "plo"/"sin" prefix
completions and "zzzzzz" (genuinely no matches) are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@mottelet mottelet changed the title Enable JupyterLab syntax highlighting via jupyterlab-scilab, fix tab completion and continuation-prompt hang Enable JupyterLab syntax highlighting, fix tab completion and continuation-prompt hang Sep 24, 2026
mottelet added a commit to mottelet/jupyterlab-scilab that referenced this pull request Sep 24, 2026
The previous README described this as an in-progress stopgap
replacement -- "not yet wired up to scilab_kernel", "not built or
tested against a real JupyterLab instance yet", scilab_kernel
"currently" using Octave mode -- none of which has been true since
Calysto/scilab_kernel#58 switched to codemirror_mode: "scilab" and
both were verified end-to-end together. Rewritten to describe the
extension as it actually is now: published, installable with a single
pip command, and picked up automatically by scilab_kernel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mottelet and others added 2 commits September 25, 2026 14:50
Scilab runtime errors (undefined variable, wrong dimensions, syntax
errors, a user's own error(...) call, ...) were printed as plain
stdout -- only the unrelated continuation-prompt hang message (a
synthetic Python-side error from metakernel) got the red stderr
treatment.

lasterror() is Scilab's own record of the last error: it returns []
if the command that just ran didn't error, or the error text
otherwise, and reading it clears it -- a reliable signal regardless
of the message's content, unlike pattern-matching (which can't tell
a user's own error("some text") apart from ordinary output).

Since a Scilab error is always the last thing printed for a command
(execution stops as soon as one occurs), Write() now holds back each
output chunk by one: a new chunk flushes the previous one immediately
as plain stdout (live streaming during long loops is unaffected),
while the true last chunk waits until the command finishes, when
lasterror() says whether it should go out via Write() or Error().

Error() itself now also wraps its message with a blank line on each
side (matching the continuation-prompt message) and pads out with
newlines instead of spaces by delegating to metakernel's own
sep-joining, and strips Scilab's own terminal control codes from the
message first -- left in, an embedded reset code cancels the red
partway through.

Verified against a live kernel: undefined variables, wrong
dimensions, syntax errors, nested-function errors, and custom
error() messages (including ones with %/quotes) all show red;
plain output, plotting, tab completion, error recovery in the next
cell, and python -m scilab_kernel.check all unaffected.

Added code_stderr to test_scilab_kernel.py so jupyter_kernel_test's
own stderr test (previously skipped) now exercises this end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Error() override added a leading/trailing blank line around
every red message for legibility, but on reflection it's not wanted:
drop it and go back to metakernel's own Error() as-is. The error
detection/coloring itself (_had_scilab_error(), _flush_pending_output())
is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant