Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:
run: msbuild VirtualTabGroups.sln /p:Configuration=Debug /p:Platform=x64

- name: Run tests
run: dotnet test VirtualTabGroups.Tests/VirtualTabGroups.Tests.csproj --nologo --no-build
run: dotnet test tests/VirtualTabGroups.Tests/VirtualTabGroups.Tests.csproj --nologo --no-build
103 changes: 103 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Contributing to Virtual Tab Groups

Thanks for considering a contribution. This file orients you to the codebase so you can get from `git clone` to a useful change without surprises.

## Project structure

```
VirtualTabGroups.sln ← Solution file
VirtualTabGroups/ ← Shipping plugin (the only project users install)
Core/ ← Pure C# library, no Notepad++ dependency
Plugin/ ← Notepad++ integration: entry points, UI, interop
Plugin/Npp/ ← Vendored Notepad++ interop types
Plugin/UI/ ← WinForms custom controls + theme manager
Plugin/Resources/ ← Embedded resources (plugin icon)
Properties/ ← AssemblyInfo, etc.
packages.config ← Legacy-style NuGet manifest
VirtualTabGroups.csproj
tests/
VirtualTabGroups.Tests/ ← Unit tests — never shipped
*Tests.cs
Fixtures/ ← Known-good JSON used by StateStore tests
VirtualTabGroups.Tests.csproj ← SDK-style csproj, references the main project
.github/workflows/ ← CI: validate.yml (PR/main) + release.yml (tag v*)
docs/ ← (none in repo — see README)
README.md
CONTRIBUTING.md ← (this file)
LICENSE
```

**Only the `VirtualTabGroups/` project ships.** Users get `VirtualTabGroups.dll` + `Newtonsoft.Json.dll` in the release zip — nothing from `tests/` or build artifacts.

## Building locally

### Prerequisites

- Windows 10 or 11
- Visual Studio 2022 Build Tools (or full IDE) with .NET Framework 4.8 SDK + targeting pack
- NuGet on PATH (or use the `nuget.exe` checked into the repo root)

### Build commands

```powershell
# Restore packages first time (or after pulling someone else's package changes)
nuget restore VirtualTabGroups.sln

# Build the plugin (Release x64 is the standard test target)
msbuild VirtualTabGroups.sln /p:Configuration=Release /p:Platform=x64

# Run the test suite
dotnet test tests/VirtualTabGroups.Tests/VirtualTabGroups.Tests.csproj --nologo

# Deploy locally for manual testing (elevated PowerShell)
Copy-Item -Force "VirtualTabGroups\bin\Release\x64\VirtualTabGroups.dll" "C:\Program Files\Notepad++\plugins\VirtualTabGroups\"
Copy-Item -Force "VirtualTabGroups\bin\Release\x64\Newtonsoft.Json.dll" "C:\Program Files\Notepad++\plugins\VirtualTabGroups\"
```

## What the test project covers (and what it doesn't)

`tests/VirtualTabGroups.Tests/` covers the **model layer** — code under `VirtualTabGroups/Core/` plus a couple of test seams in `Plugin/` (the observer dedup logic, the theme manager). Specifically:

| Test file | What it verifies |
|---|---|
| `AliasResolverTests` | Display-name aliasing (`User.php`, `User(1).php`, sparse-fill, no-extension files, multi-dot names) |
| `NodeJsonConverterTests` | JSON round-trip, polymorphic folder/file discriminator, unknown-field forward compat, error paths |
| `StateStoreTests` | Load (missing/empty/valid/corrupt/future-schema), save (debounced atomic write, failure observer notification, dirty-state preservation, shutdown semantics, observer-throws isolation) |
| `TreeMutatorTests` | Add with alias collision, Remove, Move with cyclic prevention, RemoveAllByPath case-insensitive |
| `NotepadPlusPlusObserverTests` | Save-failure dedup, MessageBox routing via `IMessageBoxProxy` seam |
| `ThemeManagerTests` | Dark vs light palette resolution via `INppMessageSender` fake |

Tests run against a **temp directory** (no shared state) and a couple of fake interfaces (`IMessageBoxProxy`, `INppMessageSender`) so they never touch real Notepad++ or pop dialogs.

**What's NOT covered by automated tests** — anything that needs a running Notepad++:
- The dockable panel rendering and docking behavior
- Custom owner-draw output (chevrons, connector lines, insertion lines, theming)
- Drag-and-drop interaction
- Context menu structure
- Keyboard shortcuts
- File-close auto-removal (the `NPPN_FILEBEFORECLOSE` plumbing)
- Double-click open via `NPPM_DOOPEN` / `NPPM_SWITCHTOFILE`

These get **manual smoke-tested** inside Notepad++. The plugin writes a diagnostic log (`%appdata%\Notepad++\plugins\config\VirtualTabGroups\plugin.log`) that breadcrumbs every notable action, so reproducing a bug usually means doing the action and checking the log.

## Filing a bug

A good bug report has three things:

1. **What you did** — the steps to reproduce, in order.
2. **What happened vs. what you expected.**
3. **The relevant section of `plugin.log`** — the file lives at `%appdata%\Notepad++\plugins\config\VirtualTabGroups\plugin.log`. Paste the last few entries that bracket the failure.

With those three, most bugs become a one-round fix.

## Pull requests

- Run `dotnet test` locally before opening the PR. The suite is fast (~1 second) and catches most regressions in the model layer.
- Keep PRs focused on one logical change. A "fix bug X + refactor Y + add feature Z" PR is harder to review than three separate ones.
- For changes to `Core/`, add or update a test alongside the change. The test suite is your safety net during the next refactor.
- For UI changes, include before/after notes in the PR description (a screenshot is great if relevant). UI bugs that pass code review but fail in Notepad++ are common; the PR description should help the reviewer mentally simulate the change.
- `[DllExport]`-decorated methods in `PluginMain` are the unmanaged interface Notepad++ calls. **Don't rename them** (the export names are referenced by Notepad++) and **don't add untyped exceptions to their signatures** — any uncaught exception there crashes the host. Wrap their bodies in try/catch and route through `ReportCrash`.

## License

By contributing, you agree your contribution is licensed under the [MIT License](LICENSE) — same as the rest of the project.
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ The panel is **purely virtual**. It does not scan your disk, mirror folders, or
- **Rename** — `F2` or context menu, edits the *display label* only; the actual file on disk is never renamed.
- **Remove** — removes the entry from the panel; **does not delete the file on disk**. Folder removals prompt for confirmation when non-empty.
- **Open / activate** — double-click a file (or press `Enter` on it) to switch to its tab in Notepad++. If the file isn't currently open, it's reopened from disk. If it was an unsaved scratch buffer that's since been closed, you get a friendly "buffer no longer open" message instead of trying to create a file at a path that doesn't exist.
- **Reveal in Explorer** — right-click a file → opens Windows Explorer at the file's parent folder with the file highlighted. Disabled for unsaved buffers (nothing to reveal).
- **Copy full path** — right-click a file → puts the file's absolute path on the clipboard. Disabled for unsaved buffers.
- **Expand All / Collapse All** — context menu actions for any folder, applies recursively to its subtree.

### Persistence
- **State survives restarts.** Your virtual tree (folders, files, expanded state, last selection) is saved to a JSON file in Notepad++'s plugin config directory and loaded again on next launch.
- **Auto-removal when files close.** Closing a file via Notepad++'s tab `X` removes it from any virtual folder it was in. Saves you from accumulating stale references.
- **Unsaved buffers are session-only.** You can absolutely add `new 17` or any other unsaved scratch buffer to a folder — useful for organizing work-in-progress alongside saved files. On Notepad++ restart, references to unsaved buffers are silently cleaned up (their data didn't survive shutdown, so the references would be dead anyway).
- **Panel visibility survives restarts.** If the panel was open when you closed Notepad++, it reopens automatically next launch — same way the built-in docked panels behave. No need to bring it up from the Plugins menu every session. The panel registers itself with Notepad++'s docking manager early enough that the saved layout from `dockingMgr.xml` applies cleanly.
- **Auto-removal when files close.** Closing a file via Notepad++'s tab `X` removes it from any virtual folder it was in. Saves you from accumulating stale references. The auto-removal is suspended during Notepad++ shutdown so the per-tab close notifications don't strip everything that was open at exit time.
- **Unsaved buffers can persist across sessions.** Add `new 17` or any other unsaved scratch buffer to a folder — useful for organizing work-in-progress alongside saved files. If Notepad++'s session backup is enabled (*Settings → Preferences → Backup → "Remember current session for next launch"*), the buffer comes back on relaunch with the same name and your virtual entry stays valid. With session backup off, the buffer is gone after shutdown and the dead reference is silently cleaned up on the next launch — saved-file entries are unaffected either way.
- **Corrupt-state recovery.** If something hand-edits or corrupts the state file, the plugin backs it up to `state.json.corrupt-yyyyMMdd-HHmmssZ-<random>` and starts with a clean tree rather than crashing. A dialog tells you where the backup landed.
- **Future-version safety.** If a future version of the plugin writes a newer schema, an older plugin reading that file enters read-only mode for the session rather than overwriting newer data.

Expand Down Expand Up @@ -199,7 +202,7 @@ For `x86` or `ARM64`, swap the `/p:Platform=` value.
### Running tests

```powershell
dotnet test VirtualTabGroups.Tests\VirtualTabGroups.Tests.csproj --nologo
dotnet test tests\VirtualTabGroups.Tests\VirtualTabGroups.Tests.csproj --nologo
```

The test project covers the model layer (StateStore, TreeMutator, NodeJsonConverter, AliasResolver, theme manager, observer) — 54 unit tests at the time of writing. UI behavior is verified manually inside Notepad++ since WinForms doesn't lend itself to automated testing without a real message loop.
Expand Down Expand Up @@ -240,17 +243,20 @@ The codebase is split into:

The model layer (`Core/`) is fully unit-tested without any Notepad++ instance. The UI layer (`Plugin/`) is exercised through manual smoke testing.

### The `tests/` directory

`tests/VirtualTabGroups.Tests/` is a separate xUnit project that covers everything under `Core/` plus the testable seams in `Plugin/` (the observer dedup logic, the theme manager). It is **never shipped** — the release zip contains only `VirtualTabGroups.dll` and `Newtonsoft.Json.dll`. The test project exists for CI (every PR and push to `main` runs `dotnet test`) and for refactoring safety. See [CONTRIBUTING.md](CONTRIBUTING.md) for a detailed breakdown of what's covered.

---

## Roadmap

These would all fit naturally with what the plugin already does. None are committed — but each one would slot in cleanly.

### Likely-soon
- **"Reveal in Explorer"** in the file context menu — opens an Explorer window with the file selected.
- **"Copy full path"** in the file context menu.
- **Search / filter box** above the tree to quickly find a file in a large workspace.
- **Customizable keybindings** — currently the menu shortcuts are baked in.
- **Customizable keybindings** — currently the menu shortcuts are baked in. (Note: the *Show panel* shortcut `Ctrl+Shift+T` is already rebindable via *Notepad++ → Settings → Shortcut Mapper → Plugin commands*. This roadmap item is specifically about the in-panel shortcuts: `F2`, `Delete`, `Ctrl+N`, etc.)
- **`Ctrl+C` to copy the selected file's path** without opening the context menu.

### Bigger ideas
- **Multiple workspaces.** A dropdown above the tree to switch between, say, "Work project A", "Personal scripts", "Documentation cleanup" — each with its own root tree, saved to its own state file. Useful when you context-switch between unrelated projects.
Expand Down Expand Up @@ -282,6 +288,10 @@ If you're interested in any of these, an issue or PR is welcome — see [Contrib
### "Plugin is not compatible" error on launch
- The most common cause is deploying a Debug build (links against `VCRUNTIME140D.dll`, a non-redistributable). Always deploy from `bin\Release\x64\`, not `bin\Debug\`.

### Notepad++ takes two clicks to relaunch after closing
- Not caused by this plugin. If you have several unsaved scratch buffers open at shutdown with session backup enabled, Notepad++ writes each one to `%appdata%\Notepad++\backup\` before the process actually exits, holding its single-instance mutex for a few extra seconds. Clicking the Notepad++ icon during that window is bounced to the dying process (invisibly) and does nothing — a second click after the process is fully gone launches a fresh instance normally.
- Confirmed with `plugin.log`: our shutdown disposal completes in ~30 ms, and the gap before the next launch is entirely Notepad++-side. Closing unused scratch buffers before quitting (or disabling session backup if you don't need it) shortens the gap.

### Things break in unexpected ways
- Open `%appdata%\Notepad++\plugins\config\VirtualTabGroups\plugin.log`. The plugin logs every notable event (startup, dialogs, exceptions with stack traces, breadcrumbs through user actions). Most issues are visible in one read.
- If you file an issue, attaching the relevant section of `plugin.log` makes a fix dramatically faster.
Expand All @@ -297,9 +307,7 @@ If you're interested in any of these, an issue or PR is welcome — see [Contrib

Bug reports, feature requests, and PRs are all welcome via [GitHub Issues](https://github.com/JamesShaver/VirtualTabGroups/issues).

When filing a bug, including the contents of `plugin.log` (or the relevant lines near the failure) helps enormously.

For PRs, please run `dotnet test` locally before opening — the test suite is fast (~1 second) and catches most regressions in the model layer.
See [CONTRIBUTING.md](CONTRIBUTING.md) for the project structure, build steps, and details on what the test project covers vs. what gets manual smoke-tested. When filing a bug, including the contents of `plugin.log` (or the relevant lines near the failure) helps enormously.

---

Expand Down
2 changes: 1 addition & 1 deletion VirtualTabGroups.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ VisualStudioVersion = 17.4.33205.214
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualTabGroups", "VirtualTabGroups\VirtualTabGroups.csproj", "{DFB2432F-5C55-4D76-A7FA-934881E5D60A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualTabGroups.Tests", "VirtualTabGroups.Tests\VirtualTabGroups.Tests.csproj", "{47970DAB-57C2-4E86-A7A7-97CA1766601E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtualTabGroups.Tests", "tests\VirtualTabGroups.Tests\VirtualTabGroups.Tests.csproj", "{47970DAB-57C2-4E86-A7A7-97CA1766601E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
8 changes: 8 additions & 0 deletions VirtualTabGroups/Plugin/Npp/NppNotif.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public enum NppNotif : uint
NPPN_LANGCHANGED = NPPN_FIRST + 11,
NPPN_WORDSTYLESUPDATED = NPPN_FIRST + 12,
NPPN_SHORTCUTREMAPPED = NPPN_FIRST + 13,
// Fires when Notepad++ has decided to shut down but BEFORE per-file
// close notifications start. Use it to suppress per-file cleanup that
// would otherwise strip every open document out of the virtual tree
// (since each open buffer closes during the shutdown sequence).
NPPN_BEFORESHUTDOWN = NPPN_FIRST + 19,
// Fires if the user backs out of shutdown (e.g., they hit Cancel on the
// "Save dirty files?" dialog). Used to clear the shutdown flag.
NPPN_CANCELSHUTDOWN = NPPN_FIRST + 20,
NPPN_DARKMODECHANGED = NPPN_FIRST + 27,
}
}
Loading
Loading