From b80736234b8b804b07b122cd48c44c0c90ff0c76 Mon Sep 17 00:00:00 2001 From: idossha Date: Sat, 12 Sep 2026 23:56:43 -0500 Subject: [PATCH 1/2] fix(debug): keep MATLAB breakpoints in Neovim with figure support Apply session-local editor suppression, repair cell ranges and tmux transport, consolidate contributor manuals, and add headless regression coverage and CI. Verified: 23 headless tests, actionlint, and git diff --check. --- .editorconfig | 19 +++ .github/workflows/tests.yml | 24 ++++ .gitignore | 3 + CHANGELOG.md | 21 ++++ CONTRIBUTIONS.md | 18 +++ MATLAB_debug.md | 112 ----------------- README.md | 67 ++-------- docs/ARCHITECTURE.md | 62 ++++++++++ docs/DEBUGGING.md | 97 +++++++++++++++ docs/DECISIONS.md | 31 +++++ docs/RELEASING.md | 20 +++ docs/ROADMAP.md | 20 +++ docs/TESTING.md | 76 ++++++++++++ ftplugin/matlab.lua | 2 + lua/matlab/cells.lua | 6 +- lua/matlab/commands.lua | 16 ++- lua/matlab/config.lua | 2 + lua/matlab/init.lua | 4 + lua/matlab/tmux.lua | 46 ++++--- tests/README.md | 236 +----------------------------------- tests/check_matlab.lua | 42 +++++++ tests/run.lua | 34 ++++++ tests/test_cells.lua | 75 ++++++++++++ tests/test_config.lua | 21 ++++ tests/test_debug.lua | 97 +++++++++++++++ tests/test_setup.lua | 59 +++++++++ tests/test_startup.lua | 39 ++++++ tests/test_tmux.lua | 54 +++++++++ 28 files changed, 872 insertions(+), 431 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/tests.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTIONS.md delete mode 100644 MATLAB_debug.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DEBUGGING.md create mode 100644 docs/DECISIONS.md create mode 100644 docs/RELEASING.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/TESTING.md create mode 100644 tests/check_matlab.lua create mode 100644 tests/run.lua create mode 100644 tests/test_cells.lua create mode 100644 tests/test_config.lua create mode 100644 tests/test_debug.lua create mode 100644 tests/test_setup.lua create mode 100644 tests/test_startup.lua create mode 100644 tests/test_tmux.lua diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5e231ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# Keep editor saves consistent with the existing Lua sources. +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +# Preserve intentional Markdown hard line breaks; do not auto-reflow prose. +trim_trailing_whitespace = false +max_line_length = 110 + +[*.m] +# Match the MATLAB fixtures so editor saves do not rewrite indentation. +indent_size = 4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..9716a1a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +# One dependency-free suite: no MATLAB license, GUI, or heavyweight build job. +name: Tests +on: + push: + pull_request: +permissions: + contents: read +jobs: + headless: + runs-on: ubuntu-24.04 + # Local suite takes about a second; cap package installation/network stalls. + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Install Neovim + run: sudo apt-get update && sudo apt-get install -y neovim + - name: Run headless regression suite + env: + NVIM_LOG_FILE: ${{ runner.temp }}/nvim.log + run: nvim --headless -u NONE -l tests/run.lua + - name: Check patch whitespace + run: git diff --check HEAD^ diff --git a/.gitignore b/.gitignore index b89f199..f8011cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ referecnce/ .DS_Store + +# Neovim diagnostics are local runtime output. +.nvimlog diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..998aa26 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +## [Unreleased] + +### Fixed + +- **Keep the MATLAB editor from opening at breakpoints** — newly launched sessions + apply a temporary editor setting while retaining figure support. Set + `suppress_editor_on_breakpoint = false` to keep MATLAB's own behavior. Saved + preferences are unchanged; releases without the setting warn and require the + manual Editor/Debugger preference. +- **Execute the selected cell accurately** — a section marker belongs to its own + cell, the first unmarked line is included, and execution stops before the next cell. +- **Keep terminal commands intact** — MATLAB executable paths support spaces and + quotes, existing tmux panes are recognized by exact ID, and code is sent literally + without adding escapes to MATLAB strings. + +### Added + +- **Contributor and testing guidance** — focused manuals and a dependency-free + headless regression suite run by CI. Live MATLAB checks remain separately opt-in. diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md new file mode 100644 index 0000000..ed98bed --- /dev/null +++ b/CONTRIBUTIONS.md @@ -0,0 +1,18 @@ +This guide covers contributions; [docs/TESTING.md](docs/TESTING.md) owns test commands +and [docs/RELEASING.md](docs/RELEASING.md) owns publication checks. + +# Contributing + +Clone the repository and add the checkout to Neovim's runtime path, then call +`require('matlab').setup()` in a disposable development configuration. Normal use +requires MATLAB and tmux; the automated Lua suite does not. + +Keep changes focused and follow the existing Lua modules and two-space indentation. +Preserve user configuration and avoid unrelated formatting changes. Add a regression +case for a behavior fix, update its authoritative documentation, and describe +user-visible changes in [CHANGELOG.md](CHANGELOG.md). + +Before opening a pull request, run the checks in the testing manual and `git diff --check`. +Describe the defect, resulting behavior, commands run, and any unverified MATLAB or +platform behavior. Include reproduction steps and versions for bug reports. Never +include license files, credentials, private workspace data, or personal startup scripts. diff --git a/MATLAB_debug.md b/MATLAB_debug.md deleted file mode 100644 index bbb683e..0000000 --- a/MATLAB_debug.md +++ /dev/null @@ -1,112 +0,0 @@ -# MATLAB Debugging in matlab.nvim - -## Overview - -matlab.nvim provides debugging support using MATLAB's native debugging commands. The plugin integrates MATLAB's debugger with Neovim through tmux. - -## Architecture - -- **Backend**: MATLAB's built-in debugger (`dbstop`, `dbcont`, `dbstep`, etc.) -- **Interface**: Neovim signs, commands, and debug sidebar -- **Execution**: All debug operations run in the tmux MATLAB pane - -## Key Features - -### Breakpoint Management -- **Visual indicators**: Red circle (●) for breakpoints, blue arrow (▶) for current line -- **Interactive toggling**: Set/clear breakpoints with `mdb` -- **Persistence**: Breakpoints maintained within session -- **Synchronization**: Neovim breakpoints sync with MATLAB debugger - -### Execution Control -- **Start/Stop**: `mds` / `mdq` -- **Stepping**: Step over (`mdn`), into (`mdi`), out (`mdo`) -- **Continue**: Run to next breakpoint with `mdc` -- **Global F-keys**: During debugging, F5/F10/F11/F12 work from ANY buffer - -### Debug Sidebar -- **Toggle**: `:MatlabDebugUI` or `mdu` -- **Shows**: Variables, call stack, and breakpoints -- **Navigation**: Press `` on stack frames or breakpoints to jump to location -- **Refresh**: Press `r` to refresh, `w` to update workspace - -## Workflow - -### Quick Start -1. Open MATLAB file in Neovim (inside tmux) -2. Start MATLAB server: `:MatlabStartServer` -3. Set breakpoints: `mdb` on desired lines -4. Start debugging: `mds` or `F5` -5. Step through code: `F10` (over) / `F11` (into) / `F12` (out) -6. Inspect state via debug sidebar: `mdu` -7. Stop debugging: `Shift+F5` or `mdq` - -### Conditional Breakpoints - -Use MATLAB's native commands directly in the tmux pane: - -```matlab -dbstop in myfile at 22 if x > 5 -dbstop in myfile at 33 if strcmp(status, 'error') -``` - -## Commands & Mappings - -### Debug Commands (`md` + key) -| Mapping | Command | Description | -|---------|---------|-------------| -| `mds` | `:MatlabDebugStart` | Start debugging session | -| `mdq` | `:MatlabDebugStop` | Stop debugging session | -| `mdc` | `:MatlabDebugContinue` | Continue to next breakpoint | -| `mdn` | `:MatlabDebugStepOver` | Step over (next line) | -| `mdi` | `:MatlabDebugStepInto` | Step into function | -| `mdo` | `:MatlabDebugStepOut` | Step out of function | -| `mdb` | `:MatlabDebugToggleBreakpoint` | Toggle breakpoint | -| `mdB` | `:MatlabDebugClearBreakpoints` | Clear all breakpoints | -| `mde` | `:MatlabDebugEval` | Evaluate expression | -| `mdu` | `:MatlabDebugUI` | Toggle debug sidebar | - -### Global F-Keys (Active During Debug Session) -| Key | Action | -|-----|--------| -| `F5` | Continue (or Start) | -| `F10` | Step Over | -| `F11` | Step Into | -| `F12` | Step Out | -| `Shift+F5` | Stop Debug | - -### Debug Sidebar Keybindings -| Key | Action | -|-----|--------| -| `q` | Close sidebar | -| `r` | Refresh display | -| `w` | Update workspace from MATLAB | -| `` | Jump to location under cursor | - -## MATLAB Commands Used - -The plugin sends these commands to MATLAB: - -- `dbstop in file at line` - Set breakpoint -- `dbclear file at line` - Clear specific breakpoint -- `dbclear all` - Clear all breakpoints -- `dbcont` - Continue execution -- `dbstep` - Step over -- `dbstep in` - Step into function -- `dbstep out` - Step out of function -- `dbstack` - Show call stack -- `dbstatus` - Show all breakpoints -- `dbquit` - Exit debug mode -- `whos` - Show workspace variables - -## Limitations - -- No conditional breakpoints via UI (use MATLAB commands directly) -- No watch expressions -- Requires tmux environment - -## Troubleshooting - -**Breakpoints not working**: Ensure line contains executable code (not comments/empty) -**Commands fail**: Verify MATLAB server is running (`:MatlabStartServer`) -**Debug line not updating**: Run `:MatlabDebugUpdateLine` to manually refresh diff --git a/README.md b/README.md index f979eb2..c4cb97b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ **matlab.nvim** is a modern Neovim plugin for MATLAB integration with tmux. -Inspired by [MortenStabenau/matlab-vim](https://github.com/MortenStabenau/matlab-vim), rewritten in Lua for Neovim. +**Contributions are welcome!** See [CONTRIBUTIONS.md](CONTRIBUTIONS.md). -**Contributions are welcome!** +Project docs: [architecture](docs/ARCHITECTURE.md), [decisions](docs/DECISIONS.md), +[testing](docs/TESTING.md), [roadmap](docs/ROADMAP.md), and [release checks](docs/RELEASING.md). ![Demo of Neovim MATLAB Plugin](docs/example.gif) @@ -14,13 +15,13 @@ Inspired by [MortenStabenau/matlab-vim](https://github.com/MortenStabenau/matlab - Access MATLAB documentation for functions - Save and load MATLAB workspace files - Native MATLAB debugger integration -- Visual breakpoint inicators -- tep-through execution (over, into, out) -- Debug siebar with variables, call stack, breakpoints +- Visual breakpoint indicators +- Step-through execution (over, into, out) +- Debug sidebar with variables, call stack, breakpoints ## Requirements -- **Neovim**: 0.7.0 or later +- **Neovim**: 0.9.0 or later - **tmux**: Must be installed and running - **MATLAB**: Any recent version @@ -57,13 +58,14 @@ require('matlab').setup({ executable = 'matlab', -- Tmux pane configuration - panel_size = 50, + panel_size = 50, -- Example override; see lua/matlab/config.lua for defaults panel_size_type = 'percentage', tmux_pane_direction = 'right', tmux_pane_focus = true, -- Behavior auto_start = true, + suppress_editor_on_breakpoint = true, default_mappings = true, minimal_notifications = true, @@ -122,53 +124,8 @@ plot(x, y); ## Debugging -### Quick Start - -1. Set breakpoints: `mdb` -2. Start debugging: `mds` (or `F5`) -3. Step through code: - - `mdc` or `F5` - Continue to next breakpoint - - `mdn` or `F10` - Step over - - `mdi` or `F11` - Step into - - `mdo` or `F12` - Step out -4. Stop debugging: `mdq` (or `Shift+F5`) - -### Debug Commands - -| Key | Command | Description | -|-----|---------|-------------| -| `mds` | `:MatlabDebugStart` | Start debugging | -| `mdq` | `:MatlabDebugStop` | Stop debugging | -| `mdc` | `:MatlabDebugContinue` | Continue execution | -| `mdn` | `:MatlabDebugStepOver` | Step over line | -| `mdi` | `:MatlabDebugStepInto` | Step into function | -| `mdo` | `:MatlabDebugStepOut` | Step out of function | -| `mdb` | `:MatlabDebugToggleBreakpoint` | Toggle breakpoint | -| `mdB` | `:MatlabDebugClearBreakpoints` | Clear all breakpoints | -| `mde` | `:MatlabDebugEval` | Evaluate expression | -| `mdu` | `:MatlabDebugUI` | Toggle debug sidebar | - -### Visual Indicators - -- **Breakpoints**: Red circle (●) with full-line highlighting -- **Current line**: Blue arrow (▶) with full-line highlighting - -### Debug Sidebar - -Toggle with `mdu` or `:MatlabDebugUI`. Shows call stack, breakpoints, and workspace variables. - -| Key | Action | -|-----|--------| -| `q` | Close sidebar | -| `r` | Refresh display | -| `w` | Update workspace from MATLAB | -| `` | Jump to location under cursor | - -### Debug Tips - -- Files auto-save when starting debug session -- Breakpoints persist within Neovim session -- Use MATLAB commands directly in tmux pane (`whos`, `dbstack`, etc.) +See [Debugging](docs/DEBUGGING.md) for the breakpoint workflow, editor suppression +while retaining plots, commands, sidebar controls, and limitations. ## Troubleshooting @@ -199,4 +156,6 @@ View logs: `~/.cache/nvim/matlab_nvim.log` ## License +Inspired by [MortenStabenau/matlab-vim](https://github.com/MortenStabenau/matlab-vim), rewritten in Lua for Neovim. + MIT diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f7c8b3e --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,62 @@ +This is the architecture contract; [README.md](../README.md) covers setup. Deviations require +updating this file and appending to [DECISIONS.md](DECISIONS.md) in the same change. +Section numbers are stable and must not be renumbered. + +# Architecture contract + +## 1. MATLAB startup + +Lua builds the MATLAB command and tmux hosts its interactive terminal, preserving +the existing architecture instead of adding a separate debugger transport. + +**Editor suppression is session-local.** `suppress_editor_on_breakpoint` defaults +to true and applies `OpenFileAtBreakpoint.TemporaryValue = false` before startup +code. False leaves MATLAB's preference untouched. Failure warns and continues, +so older MATLAB releases remain usable. No persistent preference API is used. + +**Figure support remains available.** Keep `-nodesktop -nosplash`; do not add +`-nodisplay`, `-nojvm`, or `-noFigureWindows` to suppress the editor. + +## 2. Verification and scope + +The dependency-free headless suite checks Lua behavior with controlled external +operations. [TESTING.md](TESTING.md) owns commands, fixtures, and acceptance checks. +CI runs the same suite; it cannot certify a licensed MATLAB desktop session. +Automatic navigation redesign and reconfiguring existing MATLAB panes are outside +this revision. No debugger framework or test-library dependency is introduced. + +## 3. Component boundaries + +| Component | Responsibility | +| --- | --- | +| `plugin/matlab.vim`, `ftplugin/matlab.lua` | MATLAB filetype detection and buffer-local mappings. Setup remains an explicit user call. | +| `lua/matlab/init.lua`, `config.lua` | Public commands, lifecycle autocommands, and merged configuration. | +| `lua/matlab/tmux.lua` | Executable discovery, terminal-pane lifecycle, startup arguments, and command transport. | +| `lua/matlab/commands.lua`, `cells.lua`, `workspace.lua` | Script/cell execution, cell folds, documentation, and workspace commands. | +| `lua/matlab/debug.lua` | Session state, breakpoint signs, native MATLAB debug commands, and location parsing. | +| `lua/matlab/debug_ui.lua` | Scratch-buffer sidebar, terminal-derived call stack, and temporary-file workspace exchange. | +| `lua/matlab/utils.lua` | Notifications and log output. | + +**MATLAB remains the execution engine.** Lua sends native MATLAB statements through +tmux; no MATLAB Engine or Debug Adapter Protocol connection is maintained. Debugger +location updates inspect terminal output and match open buffer basenames. This makes +terminal formatting and duplicate filenames integration limits, not protocol guarantees. + +**Configuration has one source of truth.** Defaults live in +[`config.lua`](../lua/matlab/config.lua). Setup deep-merges user options so nested +mapping overrides retain unrelated defaults. Examples in the README are configuration +examples, not a second defaults declaration. + +## 4. Documentation and change boundaries + +[DEBUGGING.md](DEBUGGING.md) owns the interactive debugger manual, +[TESTING.md](TESTING.md) owns verification, [RELEASING.md](RELEASING.md) owns publication, +and [ROADMAP.md](ROADMAP.md) records outstanding work. Historical reasons belong in +[DECISIONS.md](DECISIONS.md), not duplicate user guides. + +Public startup behavior is defined by `lua/matlab/config.lua` and +`lua/matlab/tmux.lua`; changes to the editor setting, opt-out, or launch flags must +update this contract and its regression coverage together. An absent +`suppress_editor_on_breakpoint` option uses the enabled default described in §1. +Keep shell argument escaping separate from MATLAB string escaping to avoid commands +changing meaning when paths contain spaces or quotes. diff --git a/docs/DEBUGGING.md b/docs/DEBUGGING.md new file mode 100644 index 0000000..b1a57ea --- /dev/null +++ b/docs/DEBUGGING.md @@ -0,0 +1,97 @@ +This guide covers interactive debugging; see [README](../README.md) for setup and +[TESTING](TESTING.md) for verification and known coverage gaps. + +# Debugging + +### Keep debugging in Neovim, with MATLAB plots + +New MATLAB sessions disable automatic editor opening at breakpoints using the +[documented MATLAB editor setting](https://www.mathworks.com/help/matlab/ref/matlab.editor-settings.html). +This applies only to that session; saved MATLAB preferences are unchanged. +Set `suppress_editor_on_breakpoint = false` to leave MATLAB's setting alone, +then restart the MATLAB server for the configuration change to take effect. + +The plugin uses `-nodesktop -nosplash`, which permits separate figure windows. +`-nodisplay` disables display output and should not be used if you want plots. +On Linux, a working graphical display is still required. + +For an already running MATLAB pane, execute: + +```matlab +s = settings; +s.matlab.editor.OpenFileAtBreakpoint.TemporaryValue = false; +``` + +If your MATLAB version does not expose this setting, startup prints a warning +and continues. In MATLAB's **Preferences > Editor/Debugger**, disable automatic +file opening at breakpoints (wording varies by release). This manual preference +persists across sessions. Explicitly opening a file in MATLAB remains available; +this setting does not add debugger synchronization beyond the plugin's existing +Neovim debug commands. + +### Quick Start + +1. Open a saved MATLAB file inside tmux and run `:MatlabStartServer`. +2. Set breakpoints: `mdb` +3. Start debugging: `mds` or `:MatlabDebugStart` +4. Step through code: + - `mdc` or `F5` - Continue to next breakpoint + - `mdn` or `F10` - Step over + - `mdi` or `F11` - Step into + - `mdo` or `F12` - Step out +5. Stop debugging: `mdq` (or `Shift+F5`) + +### Debug Commands + +| Key | Command | Description | +|-----|---------|-------------| +| `mds` | `:MatlabDebugStart` | Start debugging | +| `mdq` | `:MatlabDebugStop` | Stop debugging | +| `mdc` | `:MatlabDebugContinue` | Continue execution | +| `mdn` | `:MatlabDebugStepOver` | Step over line | +| `mdi` | `:MatlabDebugStepInto` | Step into function | +| `mdo` | `:MatlabDebugStepOut` | Step out of function | +| `mdb` | `:MatlabDebugToggleBreakpoint` | Toggle breakpoint | +| `mdB` | `:MatlabDebugClearBreakpoints` | Clear all breakpoints | +| `mde` | `:MatlabDebugEval` | Evaluate expression | +| `mdu` | `:MatlabDebugUI` | Toggle debug sidebar | + +### Visual Indicators + +- **Breakpoints**: Red circle (●) with full-line highlighting +- **Current line**: Blue arrow (▶) with full-line highlighting + +### Debug Sidebar + +Toggle with `mdu` or `:MatlabDebugUI`. Shows call stack, breakpoints, and workspace variables. + +| Key | Action | +|-----|--------| +| `q` | Close sidebar | +| `r` | Refresh display | +| `w` | Update workspace from MATLAB | +| `` | Jump to location under cursor | + +### Debug Tips + +- Files auto-save when starting debug session +- Breakpoints persist within Neovim session +- Use MATLAB commands directly in tmux pane (`whos`, `dbstack`, etc.) + +### Conditional breakpoints and limits + +Use MATLAB's native commands in the tmux pane for conditional breakpoints: + +```matlab +dbstop in myfile at 22 if x > 5 +dbstop in myfile at 33 if strcmp(status, 'error') +``` + +Breakpoints added directly in MATLAB are not imported into Neovim's breakpoint list. +The sidebar has no watch-expression editor. Location updates parse recent MATLAB +terminal output and match open buffers by filename; they are not a debugger protocol. +Use `:MatlabDebugUpdateLine` to retry a stale location update, or inspect `dbstack` +and `dbstatus` directly in MATLAB. Files with the same basename can be ambiguous. + +F-key mappings are installed globally during a debug session and removed when it +ends. The plugin does not restore pre-existing global F-key mappings. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md new file mode 100644 index 0000000..3ee41d4 --- /dev/null +++ b/docs/DECISIONS.md @@ -0,0 +1,31 @@ +See [the architecture contract](ARCHITECTURE.md) for current behavior and +[ROADMAP.md](ROADMAP.md) for outstanding work. + +# Decisions + +## 2026-09-12 — Suppress automatic editor opening per session + +The supplied discussion requests Neovim debugging with MATLAB figure windows. +Use the documented `OpenFileAtBreakpoint.TemporaryValue` setting before startup +code. Default suppression to enabled, with a configuration opt-out. Warn and +continue if unavailable. Persistent preferences would affect unrelated desktop +sessions; `-nodisplay` would remove the requested plots; undocumented Java +preferences would introduce a version-dependent compatibility dependency. + +Startup command checks live in `tests/test_startup.lua`. Interactive breakpoint +and visible figure behavior require MATLAB integration verification separately. + +## 2026-09-12 — Consolidate manuals and add headless regression coverage + +Keep detailed debugging and testing guidance under `docs/`, with a short testing +README redirect and removal of the obsolete root debugging manual, so command +tables have one maintained home. +Use Neovim itself to run deterministic Lua regression cases with controlled tmux +and MATLAB boundaries. This keeps local and CI checks reproducible without a test +framework or MATLAB license. Live editor and figure behavior stays a separate +integration acceptance check; a passing mock-based suite cannot prove it. + +CI uses one Ubuntu job with the distribution's Neovim package and the same local +test runner. There is no licensed MATLAB job or extra test framework; this keeps +routine validation inexpensive. Regression cases exposed and now cover cell-range +indexing, executable shell quoting, literal tmux command delivery, and pane identity. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..4864cc2 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,20 @@ +This manual covers publishing reviewed changes; [TESTING.md](TESTING.md) owns +verification and [CHANGELOG.md](../CHANGELOG.md) records user-visible changes. + +# Releasing + +This plugin is installed from Git. There is no package build or registry upload, +and no release script or version manifest is currently maintained. + +1. Review the exact diff, run the automated suite from the testing manual, and run + `git diff --check`. Record any unperformed MATLAB integration checks explicitly. +2. Update the changelog and affected manuals. Verify configuration and command + examples still match the code. +3. Push the reviewed branch and require its CI checks to pass before merging. + A green Lua suite is not evidence that live MATLAB integration passed. +4. If a versioned release is explicitly requested, choose and review its tag and + release notes separately. Publishing code does not itself create a tagged release. + +If a published change regresses behavior, fix it through a tested follow-up or revert +commit. Avoid rewriting shared history; users can pin an earlier known-good commit +while the correction is reviewed. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..881ee99 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,20 @@ +This page tracks outstanding verification and scope; [ARCHITECTURE.md](ARCHITECTURE.md) +defines current behavior and [TESTING.md](TESTING.md) describes acceptance checks. + +# Roadmap + +## What exists + +- tmux-hosted MATLAB execution, cells, workspace commands, and native debugger controls. +- Session-local editor suppression with an opt-out and figure-compatible startup flags. +- Headless Lua regression checks and consolidated contributor/debugging manuals. + +## What is next + +- Complete the MATLAB integration checklist across supported user environments, + recording releases and results before claiming editor/figure compatibility. +- Grow regression coverage around reproduced defects, especially terminal-output + variations and debugger lifecycle behavior. + +A debugger transport redesign, watch-expression UI, benchmarks, and a documentation +website are not accepted scope. Add a separate manual only when such a surface exists. diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..4f96035 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,76 @@ +This manual verifies the [architecture contract](ARCHITECTURE.md); see +[DEBUGGING.md](DEBUGGING.md) for user-facing commands and [CONTRIBUTIONS.md](../CONTRIBUTIONS.md) +for the development workflow. + +# Testing + +## Automated regression suite + +From the repository root: + +```sh +nvim --headless -u NONE -l tests/run.lua +``` + +Use Neovim 0.9 or newer for this `-l` test runner. It runs without user configuration, +a MATLAB installation, or a running tmux session. External operations are replaced +with controlled test doubles; failures must produce a nonzero exit status. +Coverage includes configuration overrides, cell selection boundaries, startup +argument quoting and editor-setting order, environment overrides, tmux pane IDs and +command transport, debugger breakpoint signs and stepping controls, and setup/mapping +registration. CI runs this same command on Ubuntu; it exercises plugin behavior +rather than launching an interactive session. + +## Optional MATLAB batch check + +With a licensed MATLAB installation, run separately from the Lua suite: + +```sh +MATLAB_NVIM_EXECUTABLE=/path/to/matlab nvim --headless -u NONE -l tests/check_matlab.lua +``` + +This explicitly opted-in check launches MATLAB in batch mode. It is not run by CI. +See the script for its assertions; batch execution does not prove interactive editor +or displayed-figure behavior. Startup or licensing failure remains a failed check, +not evidence against or for the plugin's interactive behavior. + +## MATLAB integration acceptance + +The automated Lua suite does not prove MATLAB licensing, startup compatibility, +terminal timing, editor suppression at a real breakpoint, or displayed figures. +Record MATLAB release, OS, Neovim version, and observed results when checking these. +A skipped or unavailable MATLAB check is not a passing integration test. + +For a human-run interactive check inside tmux, open [test_debug.m](../tests/test_debug.m) +and follow [DEBUGGING.md](DEBUGGING.md). Select executable statements by their content, +not fixed line numbers that move when the fixture changes: + +| Scenario | Check | +| --- | --- | +| Basic stepping | Break at `z = x + y`, inspect `x` and `y`, step, and confirm `z` is 30. | +| Loops | Break inside the summation loop; confirm the final sum is 15. | +| Branches | Confirm `value = 42` selects the `medium` branch. | +| Functions | Step into `compute_factorial`, inspect `dbstack`, step out, and confirm the result is 5040. | +| Recursion and call chains | Step through nested functions and confirm the stack grows and unwinds. | +| Expressions | Evaluate `x + y`, `sum(A(:))`, and `struct_data.name` while paused. | +| Errors | Enable the fixture's commented error and confirm execution enters its catch block. | +| Breakpoint state | Compare Neovim-created breakpoints with MATLAB `dbstatus`; clear and restart debugging. | +| Cleanup | Stop debugging and confirm the current-line indicator and temporary debug mappings are removed. | + +For the editor-suppression change, start a fresh session, stop at a breakpoint, and +confirm no editor opens. Check that `OpenFileAtBreakpoint.ActiveValue` is false while +its saved preference is unchanged. Run `figure; plot(1:3)` and confirm a separate +figure is available. Repeat with the configuration opt-out and confirm MATLAB's own +setting is left alone. Older releases without the setting must warn and keep starting. + +Automated GUI checks must run hidden and must not take the user's screen. The +interactive checklist above is for a human testing an ordinary editor session. +The scripts in this directory are fixtures, not an automated MATLAB assertion suite; +`img.m` is a plotting example, not a release gate. + +## Adding regression coverage + +Use small, deterministic buffers and independent expected commands or values. +Exercise failure paths as well as the reported defect. Keep external MATLAB checks +optional and explicitly labeled; do not replace a live integration claim with a mock. +Avoid sleeps, personal paths, and dependencies added solely for assertion helpers. diff --git a/ftplugin/matlab.lua b/ftplugin/matlab.lua index 5fa80ad..b12075f 100644 --- a/ftplugin/matlab.lua +++ b/ftplugin/matlab.lua @@ -38,6 +38,7 @@ map(prefix .. (mappings.workspace or 'w'), 'MatlabWorkspace', 'MATLAB: Show work map(prefix .. (mappings.clear_workspace or 'x'), 'MatlabClearWorkspace', 'MATLAB: Clear workspace') map(prefix .. (mappings.toggle_cell_fold or 'f'), 'MatlabToggleCellFold', 'MATLAB: Toggle cell fold') map(prefix .. (mappings.open_in_gui or 'g'), 'MatlabOpenInGUI', 'MATLAB: Open in GUI') +map(prefix .. (mappings.run_tests or 'T'), 'MatlabRunTests', 'MATLAB: Run all tests') -- ============================================================================ -- Debug Commands (md + key) @@ -76,6 +77,7 @@ vim.api.nvim_buf_create_user_command(0, 'MatlabKeymaps', function() ' ' .. p .. 'x - Clear workspace', ' ' .. p .. 'f - Toggle cell fold', ' ' .. p .. 'g - Open in GUI', + ' ' .. p .. 'T - Run all tests', '', 'Debug (' .. dp .. ' + key):', ' ' .. dp .. 's - Start debug', diff --git a/lua/matlab/cells.lua b/lua/matlab/cells.lua index e79836f..66f4cff 100644 --- a/lua/matlab/cells.lua +++ b/lua/matlab/cells.lua @@ -13,11 +13,11 @@ function M.find_current_cell() -- Search backwards for cell marker (%%...) while start_line > 1 do - start_line = start_line - 1 local line = buffer_lines[start_line] if line and line:match('^%s*%%%%') then break end + start_line = start_line - 1 end -- Search forwards for next cell marker (%%...) @@ -46,7 +46,7 @@ function M.execute_current_cell() local start_line, end_line = M.find_current_cell() -- Get cell content - local lines = vim.api.nvim_buf_get_lines(0, start_line, end_line + 1, false) + local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false) -- Filter out comment-only lines and empty lines local code_lines = {} @@ -90,7 +90,7 @@ function M.execute_to_cell() local _, end_line = M.find_current_cell() -- Get content from start to current cell - local lines = vim.api.nvim_buf_get_lines(0, 0, end_line + 1, false) + local lines = vim.api.nvim_buf_get_lines(0, 0, end_line, false) -- Filter out comment-only lines and empty lines local code_lines = {} diff --git a/lua/matlab/commands.lua b/lua/matlab/commands.lua index d30c112..dfd051c 100644 --- a/lua/matlab/commands.lua +++ b/lua/matlab/commands.lua @@ -76,13 +76,13 @@ function M.open_in_gui() -- Build a system command to open the file directly in MATLAB local cmd if vim.fn.has('mac') == 1 then - cmd = executable .. ' -r "edit ' .. filepath .. '"' + cmd = executable .. ' -desktop -r "edit ' .. filepath .. '"' elseif vim.fn.has('win32') == 1 or vim.fn.has('win64') == 1 then - cmd = executable .. ' /r "edit ' .. filepath .. '"' + cmd = executable .. ' /desktop /r "edit ' .. filepath .. '"' else -- Linux/WSL -- Note: This opens in GUI mode (doesn't use -nodesktop flag) -- DISPLAY will be inherited from the parent environment, not from the tmux pane - cmd = executable .. ' -r "edit ' .. filepath .. '"' + cmd = executable .. ' -desktop -r "edit ' .. filepath .. '"' end -- Execute the command in the background @@ -90,6 +90,14 @@ function M.open_in_gui() vim.fn.jobstart(cmd) end +-- Run all tests using runtests(pwd) +function M.run_tests() + if not tmux.exists() then + return + end + tmux.run('runtests(pwd)') +end + -- Show documentation for the word under cursor function M.doc() if not tmux.exists() then @@ -99,4 +107,4 @@ function M.doc() return tmux.run('help ' .. vim.fn.expand(''), true, true) end -return M \ No newline at end of file +return M diff --git a/lua/matlab/config.lua b/lua/matlab/config.lua index 005657e..bd3c13d 100644 --- a/lua/matlab/config.lua +++ b/lua/matlab/config.lua @@ -8,6 +8,7 @@ M.defaults = { panel_size = 30, -- Size in percentage (%) of the terminal width panel_size_type = 'percentage', -- 'percentage' or 'fixed' (fixed = columns) auto_start = true, + suppress_editor_on_breakpoint = true, -- Session-only; keeps figure windows available default_mappings = true, minimal_notifications = true, -- Only show important notifications (server start/stop and errors) tmux_pane_direction = 'right', -- Position of the tmux pane ('right', 'below') @@ -39,6 +40,7 @@ M.defaults = { clear_workspace = 'x', -- mx - Clear workspace toggle_cell_fold = 'f', -- mf - Toggle cell fold open_in_gui = 'g', -- mg - Open in MATLAB GUI + run_tests = 'T', -- mT - Run all tests -- Debug commands (md + key) debug_prefix = 'd', -- Makes md the debug prefix diff --git a/lua/matlab/init.lua b/lua/matlab/init.lua index f769a02..995da24 100644 --- a/lua/matlab/init.lua +++ b/lua/matlab/init.lua @@ -55,6 +55,10 @@ function M.setup(opts) vim.api.nvim_create_user_command('MatlabDoc', function() commands.doc() end, {}) + + vim.api.nvim_create_user_command('MatlabRunTests', function() + commands.run_tests() + end, {}) -- Cell execution commands vim.api.nvim_create_user_command('MatlabRunCell', function() diff --git a/lua/matlab/tmux.lua b/lua/matlab/tmux.lua index 50aa604..16d8e5b 100644 --- a/lua/matlab/tmux.lua +++ b/lua/matlab/tmux.lua @@ -73,16 +73,11 @@ function M.pane_exists() end -- Extract just the pane ID portion (e.g., %41 from $0:@12.%41) - local pane_id = M.server_pane:match('%%(%d+)') - if not pane_id then - -- Try alternative format - pane_id = M.server_pane:match('%%(%d+)$') - end - - if pane_id then - -- Escape the % for pattern matching - local pattern = '%%' .. pane_id - return result:find(pattern, 1, true) ~= nil -- true = plain text search, more reliable + local pane_id = M.server_pane:match('(%%%d+)$') + for line in result:gmatch('[^\r\n]+') do + if line == pane_id then + return true + end end return false @@ -128,17 +123,9 @@ function M.run(command, skip_interrupt, skip_output, use_shell_escape) M.execute("send-keys -t " .. vim.fn.shellescape(target) .. " C-c") end - local cmd = vim.fn.escape(command, '"') - - -- Use shell escaping by default, but allow disabling it for commands with special characters - local use_shell_escape = use_shell_escape ~= false -- default to true - local send_cmd - if use_shell_escape then - send_cmd = "send-keys -t " .. vim.fn.shellescape(target) .. " " .. vim.fn.shellescape(cmd) - else - -- Send without shell escaping - may work better for commands with special characters - send_cmd = "send-keys -t " .. vim.fn.shellescape(target) .. " '" .. cmd .. "'" - end + -- Shell quoting preserves MATLAB quotes/metacharacters; -l prevents tmux + -- interpreting code as key names. Legacy escape flags no longer alter text. + local send_cmd = "send-keys -l -t " .. vim.fn.shellescape(target) .. " " .. vim.fn.shellescape(command) local r = M.execute(send_cmd) M.execute("send-keys -t " .. vim.fn.shellescape(target) .. " Enter") @@ -150,7 +137,7 @@ function M.run(command, skip_interrupt, skip_output, use_shell_escape) }, function(choice) if choice == 'Yes' then -- Start server and run current command - M.start_server(false, vim.fn.escape(command, '"')) + M.start_server(false, command) -- Dismiss the "Press ENTER to continue" message M.execute('send-keys Enter') end @@ -276,6 +263,15 @@ end -- Build platform-specific MATLAB startup command function M.build_matlab_command(executable, startup_cmd, env_vars) local command_parts = {} + + -- TemporaryValue avoids changing preferences in future desktop sessions. + -- Keep this before user code, which may itself encounter a breakpoint. + if config.get('suppress_editor_on_breakpoint') then + startup_cmd = 'try; matlab_nvim_settings = settings; ' + .. 'matlab_nvim_settings.matlab.editor.OpenFileAtBreakpoint.TemporaryValue = false; ' + .. "catch; warning('matlab.nvim: Cannot suppress the MATLAB editor. Disable automatic opening at breakpoints in MATLAB Editor/Debugger preferences.'); " + .. 'end; clear matlab_nvim_settings; ' .. startup_cmd + end -- Add environment variables if provided -- User-provided environment variables take priority (e.g., DISPLAY for figures) @@ -293,9 +289,9 @@ function M.build_matlab_command(executable, startup_cmd, env_vars) end end - -- Always ensure -nodesktop -nosplash flags are added to prevent GUI from showing + -- Hide the desktop; editor opening at breakpoints is a separate setting above. -- Note: We do NOT add -nodisplay as it prevents figure windows from appearing - local matlab_command = executable .. ' -nodesktop -nosplash' + local matlab_command = vim.fn.shellescape(executable) .. ' -nodesktop -nosplash' -- Different platforms have different command line argument formats if vim.fn.has('win32') == 1 or vim.fn.has('win64') == 1 then @@ -375,7 +371,7 @@ function M.start_server(auto_start, startup_command) -- Create new pane, start matlab in it and save its id local project_root = M.get_project_root() M.notify('Project root: ' .. project_root, vim.log.levels.DEBUG) - local startup_cmd = 'cd ' .. vim.fn.shellescape(project_root) .. ';' + local startup_cmd = "cd('" .. project_root:gsub("'", "''") .. "');" -- Add command to startup if provided if startup_command then diff --git a/tests/README.md b/tests/README.md index 71112e9..aa4e42d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,233 +1,3 @@ -# Debug Testing Guide - -This directory contains test files for matlab.nvim's debugging functionality. - -## Test Files - -### test_debug.m - -Comprehensive test script that covers all debugging scenarios: -- Basic variable operations -- Arrays and matrices -- Loops (for, while) -- Conditionals (if/else) -- Function calls (simple, recursive, chained) -- Complex data types (cells, structs) -- Error handling (try/catch) - -## How to Test - -### Quick Test - -1. **Open the test file:** - ``` - nvim tests/test_debug.m - ``` - -2. **Start MATLAB server:** - ``` - :MatlabStartServer - ``` - Or press `mss` (default: Space + m + ss) - - **Note:** When you start debugging, the plugin will automatically `cd` MATLAB to the tests directory, so the file will be found correctly - -3. **Set some breakpoints:** - - Line 25: Basic variable operations (z = x + y) - - Line 35: Matrix operations - - Line 44: Inside for loop - - Line 54: Conditional branch - - Line 61: Before function call - -4. **Start debugging:** - ``` - :MatlabDebugStart - ``` - Or press `mds` - -5. **Use debug commands:** - - `mdc` - Continue to next breakpoint - - `mdo` - Step over current line - - `mdi` - Step into function - - `mdt` - Step out of function - - `mdv` - Show variables (whos) - - `mdk` - Show call stack (dbstack) - - `mdx` - Evaluate expression - -6. **Stop debugging when done:** - ``` - :MatlabDebugStop - ``` - Or press `mde` - -### Comprehensive Test Scenarios - -#### Test 1: Basic Stepping -1. Set breakpoint on line 25 (z = x + y) -2. Start debug -3. Use `mdv` to view variables -4. Press `mdo` to step over -5. Check that z is now calculated - -#### Test 2: Loop Iteration -1. Set breakpoint on line 44 (sum_val = sum_val + i) -2. Start debug -3. Press `mdc` to continue to breakpoint -4. Press `mdo` multiple times to watch loop iterations -5. Use `mdv` to see i and sum_val change - -#### Test 3: Function Stepping -1. Set breakpoint on line 61 (factorial_result = ...) -2. Start debug, continue to breakpoint -3. Press `mdi` to step INTO compute_factorial function -4. Use `mdk` to see call stack -5. Press `mdt` to step OUT of function - -#### Test 4: Recursive Functions -1. Set breakpoint inside compute_factorial (line 123) -2. Start debug on a factorial call -3. Step into the recursive calls -4. Use `mdk` to see growing call stack -5. Watch the stack shrink as recursion unwinds - -#### Test 5: Function Call Chain -1. Set breakpoints in function_chain_* functions -2. Call function_chain_start -3. Step through the entire chain: start → middle → end -4. Use call stack to see the chain - -#### Test 6: Expression Evaluation -1. Stop at any breakpoint -2. Press `mdx` -3. Enter expressions like: - - `x + y` - - `sum(A(:))` - - `length(cell_data)` - - `struct_data.name` - -#### Test 7: Conditional Debugging -1. Set breakpoints in if/else branches (lines 52-56) -2. Change the value variable to test different paths -3. Verify correct branch is taken - -## Debug Commands Reference - -| Command | Mapping | Description | -|---------|---------|-------------| -| `:MatlabDebugStart` | `mds` | Start debugging session | -| `:MatlabDebugStop` | `mde` | Stop debugging session | -| `:MatlabDebugContinue` | `mdc` | Continue to next breakpoint | -| `:MatlabDebugStepOver` | `mdo` | Step over (execute line) | -| `:MatlabDebugStepInto` | `mdi` | Step into function | -| `:MatlabDebugStepOut` | `mdt` | Step out of function | -| `:MatlabDebugToggleBreakpoint` | `mdb` | Toggle breakpoint | -| `:MatlabDebugClearBreakpoints` | `mdd` | Clear all breakpoints | -| `:MatlabDebugShowVariables` | `mdv` | Show variables (whos) | -| `:MatlabDebugShowStack` | `mdk` | Show call stack (dbstack) | -| `:MatlabDebugShowBreakpoints` | `mdp` | Show breakpoints (dbstatus) | -| `:MatlabDebugEval` | `mdx` | Evaluate expression | - -## What to Verify - -When testing, verify that: - -1. **Breakpoints work correctly:** - - ✓ Red circle (●) appears in sign column - - ✓ Execution stops at breakpoint - - ✓ Can toggle breakpoints on/off - - ✓ Can clear all breakpoints - -2. **Stepping works:** - - ✓ Step over executes current line - - ✓ Step into enters function calls - - ✓ Step out returns from function - - ✓ Continue runs to next breakpoint - -3. **Variable inspection:** - - ✓ `whos` shows all variables - - ✓ Can see variable values in MATLAB pane - - ✓ Variables update as you step - -4. **Call stack:** - - ✓ `dbstack` shows current execution location - - ✓ Stack grows with function calls - - ✓ Stack shrinks when returning - -5. **Breakpoint persistence:** - - ✓ Breakpoints persist across debug sessions - - ✓ Breakpoints restore when restarting debug - - ✓ Signs stay visible - -6. **Expression evaluation:** - - ✓ Can evaluate simple expressions - - ✓ Can inspect variable properties - - ✓ Results show in MATLAB pane - -## Troubleshooting Test Issues - -**Breakpoint doesn't stop execution:** -- Ensure the line has executable code (not blank/comment) -- Check breakpoint is set: `:MatlabDebugShowBreakpoints` -- Verify file is saved - -**Can't see variables:** -- Run `:MatlabDebugShowVariables` -- Check MATLAB pane for output -- Ensure you're in debug mode (paused at breakpoint) - -**Step commands don't work:** -- Make sure debug session is active -- Check you're paused at a line (not just running) -- Try `:MatlabDebugStop` and restart - -**MATLAB pane not showing output:** -- Verify MATLAB server is running -- Check tmux pane exists -- Try sending command manually in tmux pane - -## Advanced Testing - -### Test Breakpoint Sync -1. Set breakpoints in Neovim -2. In MATLAB pane, type `dbstatus` to verify -3. Add breakpoint directly in MATLAB: `dbstop in test_debug at 50` -4. Note: UI won't show this one (it's MATLAB-only) - -### Test Multiple Files -1. Create another .m file that calls functions from test_debug.m -2. Set breakpoints in both files -3. Debug through cross-file function calls - -### Test Error Conditions -1. Uncomment the `error()` call in test 11 -2. Debug through try/catch -3. Verify execution goes to catch block - -## Manual MATLAB Commands - -You can also type debug commands directly in the MATLAB tmux pane: - -```matlab -dbstop in test_debug at 25 % Set breakpoint -dbstatus % Show all breakpoints -dbcont % Continue -dbstep % Step over -dbstep in % Step into -dbstep out % Step out -dbstack % Show call stack -whos % Show variables -dbquit % Quit debug mode -dbclear all % Clear all breakpoints -``` - -## Success Criteria - -A successful test run should demonstrate: -- ✓ All 12 test sections execute correctly -- ✓ Breakpoints can be set/cleared/toggled -- ✓ Step over/into/out work as expected -- ✓ Variables are visible and update correctly -- ✓ Call stack shows correct function hierarchy -- ✓ Expression evaluation works -- ✓ No errors or crashes -- ✓ Debug session can be stopped cleanly +Test commands, fixtures, acceptance checks, and coverage limits live in +[docs/TESTING.md](../docs/TESTING.md). User-facing debugger instructions are in +[docs/DEBUGGING.md](../docs/DEBUGGING.md). diff --git a/tests/check_matlab.lua b/tests/check_matlab.lua new file mode 100644 index 0000000..01f95f2 --- /dev/null +++ b/tests/check_matlab.lua @@ -0,0 +1,42 @@ +-- Explicit licensed integration check; separate from the default suite and CI. +-- MATLAB executes the exact startup statement emitted through a real shell. +vim.opt.rtp:prepend(vim.fn.getcwd()) +local executable = vim.env.MATLAB_NVIM_EXECUTABLE +if not executable or executable == '' then + print('SKIP: set MATLAB_NVIM_EXECUTABLE to a licensed MATLAB executable') + vim.cmd('qa!') + return +end +local root = vim.fn.tempname() +vim.fn.mkdir(root, 'p') +local capture = root .. '/capture' +vim.fn.writefile({ '#!/bin/sh', [[printf '%s\n' "$@"]] }, capture) +vim.fn.setfperm(capture, 'rwx------') +require('matlab.config').setup({}) +local command = require('matlab.tmux').build_matlab_command(capture, '', {}) +local args = vim.fn.systemlist(command) +assert(vim.v.shell_error == 0 and #args == 4, 'could not decode startup arguments') +local script = root .. '/check_startup.m' +vim.fn.writefile({ + 's = settings;', + 'p = s.matlab.editor.OpenFileAtBreakpoint;', + 'hadPersonal = hasPersonalValue(p);', + 'if hadPersonal; savedPersonal = p.PersonalValue; end;', + args[4], + 'assert(p.ActiveValue == false);', + 'assert(hasPersonalValue(p) == hadPersonal);', + 'if hadPersonal; assert(isequal(p.PersonalValue, savedPersonal)); end;', + "assert(exist('matlab_nvim_settings', 'var') == 0);", + "f = figure('Visible', 'off'); plot(1:3);", + "assert(numel(findobj(f, 'Type', 'line')) == 1); close(f);", + "disp('PASS: temporary setting, saved preference, workspace cleanup, hidden plot');", +}, script) +local output = vim.fn.system({ executable, '-batch', "run('" .. script:gsub("'", "''") .. "')" }) +local status = vim.v.shell_error +print(output) +vim.fn.delete(root, 'rf') +if status ~= 0 then + print('FAIL: MATLAB integration exited with ' .. status) + vim.cmd('cquit 1') +end +vim.cmd('qa!') diff --git a/tests/run.lua b/tests/run.lua new file mode 100644 index 0000000..4d7ecfc --- /dev/null +++ b/tests/run.lua @@ -0,0 +1,34 @@ +-- Dependency-free behavioral suite. Run from the repository root with Neovim 0.9+. +vim.opt.rtp:prepend(vim.fn.getcwd()) +vim.o.swapfile = false +vim.o.shadafile = 'NONE' +vim.ui.select = function() error('Unexpected interactive selection in headless tests') end +vim.ui.input = function() error('Unexpected interactive input in headless tests') end +local passed, failed = 0, 0 +local function test(name, fn) + local ok, err = xpcall(fn, debug.traceback) + if ok then + passed = passed + 1 + print('PASS ' .. name) + else + failed = failed + 1 + print('FAIL ' .. name .. '\n' .. err) + end +end +local suites = vim.fn.glob('tests/test_*.lua', false, true) +assert(#suites > 0, 'No test suites found') +table.sort(suites) +for _, path in ipairs(suites) do + local ok, register = pcall(dofile, path) + if ok and type(register) == 'function' then + register(test) + else + failed = failed + 1 + print('FAIL loading ' .. path .. ': ' .. tostring(register)) + end +end +print(string.format('%d passed, %d failed', passed, failed)) +if failed > 0 or passed == 0 then + vim.cmd('cquit 1') +end +vim.cmd('qa!') diff --git a/tests/test_cells.lua b/tests/test_cells.lua new file mode 100644 index 0000000..8bdd6ee --- /dev/null +++ b/tests/test_cells.lua @@ -0,0 +1,75 @@ +-- Cell boundary/execution regressions, 2026-09-12. +-- Ground truth: authored MATLAB sections; row numbers count the literal buffers +-- below. Run through tests/run.lua. Live MATLAB/tmux behavior is tested separately. +local cells = require('matlab.cells') +local tmux = require('matlab.tmux') + +local function with_buffer(lines, row, check) + local previous = vim.api.nvim_get_current_buf() + local buffer = vim.api.nvim_create_buf(false, true) + local original_exists, original_run = tmux.exists, tmux.run + local sent = {} + tmux.exists = function() return true end + tmux.run = function(code) sent[#sent + 1] = code end + local ok, err = xpcall(function() + vim.api.nvim_set_current_buf(buffer) + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines) + vim.bo.filetype = 'matlab' + vim.bo.modified = false + vim.api.nvim_win_set_cursor(0, { row, 0 }) + check(sent) + end, debug.traceback) + tmux.exists, tmux.run = original_exists, original_run + vim.api.nvim_set_current_buf(previous) + vim.api.nvim_buf_delete(buffer, { force = true }) + assert(ok, err) +end + +return function(test) + test('cell marker belongs to the section it starts', function() + with_buffer({ '%% first', 'a = 1;', '%% second', 'b = 2;' }, 3, function() + assert(vim.deep_equal({ cells.find_current_cell() }, { 3, 4 })) + end) + end) + + test('adjacent and final markers are valid empty cells', function() + with_buffer({ 'a = 1;', '%% empty', '%% last' }, 2, function() + assert(vim.deep_equal({ cells.find_current_cell() }, { 2, 2 })) + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + assert(vim.deep_equal({ cells.find_current_cell() }, { 3, 3 })) + assert(vim.deep_equal(cells.get_all_cells(), { + { start = 1, ending = 1, title = 'Beginning of file' }, + { start = 2, ending = 2, title = 'empty' }, + { start = 3, ending = 3, title = 'last' }, + })) + end) + end) + + test('current cell includes first unmarked line and excludes next section', function() + with_buffer({ 'a = 1;', '%% next', 'b = 2;' }, 1, function(sent) + cells.execute_current_cell() + assert(vim.deep_equal(sent, { 'a = 1;' })) + end) + end) + + test('current cell on its marker sends only its executable lines', function() + with_buffer({ 'a = 1;', 'a = a + 1;', ' %% next', '% comment', '', 'b = 2;', '%% later', 'c = 3;' }, 3, function(sent) + cells.execute_current_cell() + assert(vim.deep_equal(sent, { 'b = 2;' })) + end) + end) + + test('execute to cell includes preceding sections and stops at boundary', function() + with_buffer({ 'a = 1;', '%% next', 'b = 2;', '%% last', 'c = 3;' }, 2, function(sent) + cells.execute_to_cell() + assert(vim.deep_equal(sent, { 'a = 1;\nb = 2;' })) + end) + end) + + test('last cell executes through the last buffer line', function() + with_buffer({ 'a = 1;', '%% last', 'b = 2;', 'c = 3;' }, 4, function(sent) + cells.execute_current_cell() + assert(vim.deep_equal(sent, { 'b = 2;\nc = 3;' })) + end) + end) +end diff --git a/tests/test_config.lua b/tests/test_config.lua new file mode 100644 index 0000000..35c0e99 --- /dev/null +++ b/tests/test_config.lua @@ -0,0 +1,21 @@ +-- Authored configuration behavior; no MATLAB or external tools required. +return function(test) + local config = require('matlab.config') + test('configuration preserves unspecified nested defaults', function() + config.setup({ auto_start = false, mappings = { run = 'R' } }) + assert(config.get('auto_start') == false) + assert(config.get('mappings').run == 'R') + assert(config.get('mappings').debug_continue == 'c') + config.setup({}) + assert(config.get('auto_start') == true) + assert(config.get('mappings').run == 'r') + end) + test('configuration does not mutate caller options or defaults', function() + local opts = { debug_ui = { sidebar_width = 60 } } + config.setup(opts) + config.options.debug_ui.sidebar_width = 80 + assert(opts.debug_ui.sidebar_width == 60) + assert(config.defaults.debug_ui.sidebar_width == 40) + config.setup({}) + end) +end diff --git a/tests/test_debug.lua b/tests/test_debug.lua new file mode 100644 index 0000000..eef7741 --- /dev/null +++ b/tests/test_debug.lua @@ -0,0 +1,97 @@ +-- Debug command/sign regressions, 2026-09-12. Ground truth: authored MATLAB +-- debugger commands and expected sign rows in the literal buffer below. +-- Run: nvim --headless -u NONE -l tests/run.lua. Live MATLAB is separate. +local debugger = require('matlab.debug') +local tmux = require('matlab.tmux') + +local function with_debug_buffer(check) + local previous = vim.api.nvim_get_current_buf() + local buffer = vim.api.nvim_create_buf(false, true) + local old_exists, old_run = tmux.exists, tmux.run + local old_active, old_breakpoints = debugger.debug_active, debugger.breakpoints + local sent = {} + tmux.exists = function() return true end + tmux.run = function(...) sent[#sent + 1] = { ... } end + debugger.debug_active, debugger.breakpoints = false, {} + local ok, err = xpcall(function() + vim.api.nvim_set_current_buf(buffer) + vim.api.nvim_buf_set_name(buffer, vim.fn.tempname() .. '/debug_fixture.m') + vim.api.nvim_buf_set_lines(buffer, 0, -1, false, { 'a = 1;', 'b = 2;', 'c = 3;' }) + vim.bo.filetype = 'matlab' + vim.bo.modified = false + debugger.setup_signs() + check(buffer, sent) + end, debug.traceback) + -- Step/continue callbacks are deferred by up to 200 ms. Disable the session + -- before draining them so no location retries or real transport can escape. + debugger.debug_active = false + vim.wait(250, function() return false end, 10) + debugger.debug_active, debugger.breakpoints = old_active, old_breakpoints + tmux.exists, tmux.run = old_exists, old_run + vim.api.nvim_set_current_buf(previous) + vim.api.nvim_buf_delete(buffer, { force = true }) + assert(ok, err) +end + +local function signs(buffer, group) + return vim.fn.sign_getplaced(buffer, { group = group })[1].signs +end + +return function(test) + test('breakpoint toggle places and removes the matching sign and MATLAB breakpoint', function() + with_debug_buffer(function(buffer, sent) + vim.api.nvim_win_set_cursor(0, { 2, 0 }) + debugger.toggle_breakpoint() + local placed = signs(buffer, debugger.sign_group_bp) + assert(#placed == 1 and placed[1].lnum == 2 and placed[1].name == 'matlab_breakpoint') + assert(debugger.breakpoints[buffer][2] == true) + debugger.toggle_breakpoint() + assert(#signs(buffer, debugger.sign_group_bp) == 0) + assert(debugger.breakpoints[buffer][2] == nil) + assert(vim.deep_equal(sent, { + { 'dbstop in debug_fixture at 2', false, false }, + { 'dbclear debug_fixture at 2', false, false }, + })) + end) + end) + + test('clearing breakpoints preserves the current debug line sign', function() + with_debug_buffer(function(buffer, sent) + debugger.toggle_breakpoint() + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + debugger.toggle_breakpoint() + assert(#signs(buffer, debugger.sign_group_bp) == 2) + vim.fn.sign_place(999999, debugger.sign_group_line, 'matlab_debug_line', buffer, { lnum = 2 }) + debugger.clear_breakpoints() + assert(#signs(buffer, debugger.sign_group_bp) == 0) + assert(vim.deep_equal(debugger.breakpoints, {})) + local location = signs(buffer, debugger.sign_group_line) + assert(#location == 1 and location[1].lnum == 2) + assert(vim.deep_equal(sent[#sent], { 'dbclear all', false, false })) + end) + end) + + test('continue and stepping preserve the active debugger without Ctrl-C', function() + with_debug_buffer(function(_, sent) + debugger.debug_active = true + debugger.continue_debug() + debugger.step_over() + debugger.step_into() + debugger.step_out() + assert(vim.deep_equal(sent, { + { 'dbcont', true, true }, { 'dbstep', true, true }, + { 'dbstep in', true, true }, { 'dbstep out', true, true }, + })) + end) + end) + + test('inactive debugger refuses continue and stepping commands', function() + with_debug_buffer(function(_, sent) + debugger.continue_debug() + debugger.step_over() + debugger.step_into() + debugger.step_out() + assert(#sent == 0) + end) + end) +end diff --git a/tests/test_setup.lua b/tests/test_setup.lua new file mode 100644 index 0000000..6cadf7e --- /dev/null +++ b/tests/test_setup.lua @@ -0,0 +1,59 @@ +-- Public setup/command/mapping contract, exercised in real Neovim buffers. +return function(test) + local plugin = require('matlab') + local tmux = require('matlab.tmux') + test('setup registers commands and routes MATLAB test execution', function() + local exists, run = tmux.exists, tmux.run + local sent = {} + tmux.exists = function() return true end + tmux.run = function(command) sent[#sent + 1] = command end + local ok, err = xpcall(function() + plugin.setup({ auto_start = false }) + local commands = vim.api.nvim_get_commands({ builtin = false }) + for _, name in ipairs({ 'MatlabRun', 'MatlabRunTests', 'MatlabDebugStart', 'MatlabDebugContinue', 'MatlabRunCell' }) do + assert(commands[name], name .. ' missing') + end + vim.cmd('MatlabRunTests') + assert(vim.deep_equal(sent, { 'runtests(pwd)' })) + plugin.setup({ auto_start = false }) + assert(#vim.api.nvim_get_autocmds({ group = 'matlab_nvim', event = 'FileType' }) == 1) + end, debug.traceback) + tmux.exists, tmux.run = exists, run + vim.api.nvim_del_augroup_by_name('matlab_nvim') + vim.api.nvim_del_augroup_by_name('MatlabDebug') + assert(ok, err) + end) + test('custom mappings stay buffer-local and preserve default debug mappings', function() + local previous = vim.api.nvim_get_current_buf() + local buffer = vim.api.nvim_create_buf(false, true) + local ok, err = xpcall(function() + vim.api.nvim_set_current_buf(buffer) + plugin.config.setup({ mappings = { prefix = ',m', run = 'R' } }) + dofile('ftplugin/matlab.lua') + local maps = {} + for _, map in ipairs(vim.api.nvim_buf_get_keymap(buffer, 'n')) do maps[map.lhs] = map.rhs end + assert(maps[',mR'] == 'MatlabRun') + assert(maps[',mdc'] == 'MatlabDebugContinue') + assert(maps[',mT'] == 'MatlabRunTests') + for _, map in ipairs(vim.api.nvim_get_keymap('n')) do assert(map.lhs ~= ',mR') end + end, debug.traceback) + vim.api.nvim_set_current_buf(previous) + vim.api.nvim_buf_delete(buffer, { force = true }) + plugin.config.setup({}) + assert(ok, err) + end) + test('mapping opt-out creates no plugin buffer mappings', function() + local previous = vim.api.nvim_get_current_buf() + local buffer = vim.api.nvim_create_buf(false, true) + local ok, err = xpcall(function() + vim.api.nvim_set_current_buf(buffer) + plugin.config.setup({ default_mappings = false }) + dofile('ftplugin/matlab.lua') + assert(#vim.api.nvim_buf_get_keymap(buffer, 'n') == 0) + end, debug.traceback) + vim.api.nvim_set_current_buf(previous) + vim.api.nvim_buf_delete(buffer, { force = true }) + plugin.config.setup({}) + assert(ok, err) + end) +end diff --git a/tests/test_startup.lua b/tests/test_startup.lua new file mode 100644 index 0000000..f8f1934 --- /dev/null +++ b/tests/test_startup.lua @@ -0,0 +1,39 @@ +-- Test the emitted shell command as a public transport contract, using a real +-- shell and fake executable. MATLAB semantics are checked separately. +return function(test) + local config = require('matlab.config') + local tmux = require('matlab.tmux') + local root = vim.fn.tempname() .. " startup's files" + vim.fn.mkdir(root, 'p') + local executable = root .. '/fake matlab' + vim.fn.writefile({ '#!/bin/sh', [[printf '%s\n' "$MATLAB_TEST_VALUE" "$@"]] }, executable) + vim.fn.setfperm(executable, 'rwx------') + local function startup_args(opts, env) + config.setup(opts) + local command = tmux.build_matlab_command(executable, "disp('user startup');", env or {}) + local args = vim.fn.systemlist(command) + assert(vim.v.shell_error == 0, table.concat(args, '\n')) + return args + end + test('startup quotes executable paths and preserves figure-compatible arguments', function() + local args = startup_args({}) + assert(#args == 5, vim.inspect(args)) + assert(args[2] == '-nodesktop' and args[3] == '-nosplash') + assert(args[4] == '-r' or args[4] == '/r') + local setting = assert(args[5]:find('OpenFileAtBreakpoint.TemporaryValue = false', 1, true)) + assert(setting < assert(args[5]:find("disp('user startup');", 1, true))) + assert(not args[5]:find('PersonalValue', 1, true)) + end) + test('startup opt-out leaves user code untouched', function() + assert(startup_args({ suppress_editor_on_breakpoint = false })[5] == "disp('user startup');") + end) + test('startup transports shell metacharacters literally in environment values', function() + local value = [[a'b "quoted" $HOME $(echo unsafe); `echo unsafe`]] + assert(startup_args({}, { MATLAB_TEST_VALUE = value })[1] == value) + end) + test('invalid environment names are ignored', function() + assert(startup_args({}, { ['BAD; echo unsafe'] = 'value' })[1] == '') + end) + vim.fn.delete(root, 'rf') + config.setup({}) +end diff --git a/tests/test_tmux.lua b/tests/test_tmux.lua new file mode 100644 index 0000000..b56bd07 --- /dev/null +++ b/tests/test_tmux.lua @@ -0,0 +1,54 @@ +-- Exercise pane identity and emitted transport arguments at the tmux boundary. +return function(test) + local tmux = require('matlab.tmux') + local function with_transport(fn) + local execute, pane = tmux.execute, tmux.server_pane + local sent = {} + tmux.server_pane = '$0:@1.%4' + tmux.execute = function(cmd) + if cmd:find('list-panes', 1, true) then return '%4\n%41\n' end + sent[#sent + 1] = cmd + return '' + end + local ok, err = xpcall(function() fn(sent) end, debug.traceback) + tmux.execute, tmux.server_pane = execute, pane + assert(ok, err) + end + test('pane identity matches an exact pane in the list', function() + with_transport(function() + assert(tmux.pane_exists()) + tmux.server_pane = '%41' + assert(tmux.pane_exists()) + tmux.server_pane = '%1' + assert(not tmux.pane_exists()) + end) + end) + test('missing pane is not confused with a longer pane ID', function() + with_transport(function() + tmux.execute = function() return '%41\n' end + assert(not tmux.pane_exists()) + end) + end) + test('MATLAB command text survives shell transport unchanged', function() + with_transport(function(sent) + local code = [[disp("a'b $HOME $(echo unsafe)");]] + tmux.run(code, true) + assert(#sent == 2, vim.inspect(sent)) + -- Replace only the tmux command with printf; let the real shell decode args. + local args = vim.fn.systemlist([[printf '%s\n' ]] .. sent[1]:sub(#'send-keys ' + 1)) + assert(vim.v.shell_error == 0) + assert(args[1] == '-l', 'tmux must send literal text, not key names') + assert(args[#args] == code, vim.inspect(args)) + assert(sent[2]:find('Enter', 1, true)) + end) + end) + test('debug continuation skips interrupt while ordinary commands interrupt', function() + with_transport(function(sent) + tmux.run('dbcont', true) + assert(#sent == 2) + tmux.run('x = 1') + assert(#sent == 5) + assert(sent[3]:find('C-c', 1, true)) + end) + end) +end From a6e82c81d1f394d7a7023613fa22c44324edcc38 Mon Sep 17 00:00:00 2001 From: idossha Date: Sat, 12 Sep 2026 23:57:47 -0500 Subject: [PATCH 2/2] docs: remove trailing whitespace from the log reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71d3522..7bec229 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ require('matlab').setup({ ``` Check configuration: `:MatlabShowConfig` -View logs: `~/.cache/nvim/matlab_nvim.log` +View logs: `~/.cache/nvim/matlab_nvim.log` Inspired by [MortenStabenau/matlab-vim](https://github.com/MortenStabenau/matlab-vim), rewritten in Lua for Neovim.