|
| 1 | +# PR Summary — Query Playground connections across save / reopen + Connect picker |
| 2 | + |
| 3 | +> PR: [microsoft/vscode-documentdb#758](https://github.com/microsoft/vscode-documentdb/pull/758) |
| 4 | +> Iterative working summary of what was implemented and **why**. The "why" is the |
| 5 | +> point of this doc — it captures the constraints and design forks so future |
| 6 | +> reviews/analysis don't have to re-derive them. |
| 7 | +
|
| 8 | +## Goal |
| 9 | + |
| 10 | +Make a Query Playground's connection (the cluster + database it runs against) **survive |
| 11 | +the things that previously silently dropped it**, and give the user a first-class way to |
| 12 | +(re)connect a playground when no connection is in context. |
| 13 | + |
| 14 | +## Background — the original problem |
| 15 | + |
| 16 | +`PlaygroundService` binds each `.documentdb.js` document to a `PlaygroundConnection` |
| 17 | +(stable `clusterId` + `databaseName` + display/tree context) in an **in-memory map keyed |
| 18 | +by the document URI string**. Query execution reads that binding to know where to run. |
| 19 | + |
| 20 | +Because the key is the URI and the map is never persisted, the binding is lost whenever: |
| 21 | + |
| 22 | +- **untitled → file** — saving a scratch playground to disk (scheme/path changes), |
| 23 | +- **file → file (Save As)** — re-saving under a new name (path changes), or |
| 24 | +- **reopening a saved playground in a new VS Code session** — the map starts empty. |
| 25 | + |
| 26 | +In every case the file opens **disconnected** with no obvious recovery, which is confusing |
| 27 | +because the document looks identical to when it worked. |
| 28 | + |
| 29 | +## Two distinct sub-problems, two mechanisms |
| 30 | + |
| 31 | +The same-session URI change and the cross-session reopen are **fundamentally different** |
| 32 | +and cannot share one solution: |
| 33 | + |
| 34 | +| Sub-problem | Is there state to recover from? | Mechanism | |
| 35 | +| ------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------- | |
| 36 | +| untitled→file, Save As (same session) | **Yes** — the source document is still open and still holds the connection | Content-correlated **transfer at save time** | |
| 37 | +| Reopen in a fresh session | **No** — the process restarted; both the binding map _and_ the credential cache are empty | Must be an **actionable affordance** (the Connect picker) | |
| 38 | + |
| 39 | +> **Key realization that shaped the design:** after a real restart there is nothing in |
| 40 | +> memory to reconnect _to_ (credentials are gone too), so persistence (a Memento) or an |
| 41 | +> in-file hint would be the _only_ ways to auto-restore — both were explicitly ruled out |
| 42 | +> as too heavy / user-visible. That leaves an **actionable connect** as the only viable |
| 43 | +> path for the cross-session case, which is why the bulk of this PR is a good picker UX |
| 44 | +> rather than a persistence layer. |
| 45 | +
|
| 46 | +## What changed (by area) |
| 47 | + |
| 48 | +| Area | Change | Why | |
| 49 | +| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
| 50 | +| `PlaygroundService.transferConnectionToSavedFile` | On save of a `file:` playground, find the still-open source playground whose content matches and re-key its connection onto the saved URI. Guards: skip if the saved file is already bound; require **exactly one** content match (ambiguity guard for "Save All"); size-gate via `offsetAt`-based length before any `getText()`. | Covers untitled→file and Save As without persistence, using the only reliable correlation available at that moment: content, while the source is still open. Save (not open) is used because a freshly-created file's model is still **empty** for ~150 ms after `onDidOpen`. | |
| 51 | +| New `pickTreeNode` (`utils/pickItem/pickTreeNode.ts`) | A generic, reusable quick pick that **browses the live Connections tree** (folders → clusters → databases) by repeatedly calling the tree's own `getChildren`. | Reuses folder nesting, ordering, icons, and **connect/auth-on-expand** instead of re-implementing them; avoids eagerly connecting to clusters the user didn't choose. Mirrors vscode-cosmosdb's `pickWorkspaceResource` shape. | |
| 52 | +| `playground.connect` command (`connectDatabase.ts`) | Prompts via `pickTreeNode` (leaf = `treeItem_database`) and binds the playground. Wired to the previously dead-end **"Not connected" CodeLens and StatusBar**. | Turns the always-visible disconnected indicator into a one-click connect — the primary recovery path for the cross-session case. | |
| 53 | +| Connect-on-run (`executePlaygroundCode.ts`) | Running an **unconnected** playground now launches the connect picker and continues the run on success (cancel = no-op). | The most contextual moment to ask for a connection is when the user actually tries to use one; replaces a dead-end info message. | |
| 54 | +| Switch-database modal (`connectDatabase.ts`) | Clicking the connection lens **while connected** opens a modal offering to switch databases. The existing binding is only replaced **after** a new database is selected. | Lets users repoint a playground, while guaranteeing that **aborting the picker preserves the current connection** (no "disconnect first, then maybe fail to pick"). | |
| 55 | +| In-session restore (`PlaygroundService`) | A **non-persistent** in-memory "last binding per URI" map survives document _close_; on reopen within the same session the binding is restored **iff** the target cluster still has live credentials (`CredentialCache.hasCredentials`). Cleared on explicit `removeConnection` and on dispose. | Cheap quality-of-life win for close→reopen in one session. Deliberately **not** persistence (no Memento, no file mutation), so it is neither approach "A" nor "C"; a true restart correctly falls through to the Connect affordance. Never reads the document body, so it is immune to the empty-model open race. | |
| 56 | + |
| 57 | +## Dual-ID compliance |
| 58 | + |
| 59 | +All cache/credential lookups use the stable **`clusterId`** (never `treeId`), per the |
| 60 | +repo's dual-ID rule, so bindings keep working when a connection is moved between folders. |
| 61 | +`PlaygroundConnection` stores `clusterId`; the picker reads it from the selected |
| 62 | +`DatabaseItem`'s `cluster.clusterId`. |
| 63 | + |
| 64 | +## Picker UX decisions (and why) |
| 65 | + |
| 66 | +These were iterated with the maintainer; the rationale matters for future tweaks: |
| 67 | + |
| 68 | +- **Loading indicator** — picks are passed to `showQuickPick` as a _promise_ so the quick |
| 69 | + pick stays visible with a busy spinner while a level loads (a cluster connection can take |
| 70 | + seconds). Earlier the quick pick vanished during the wait, which read as a crash. |
| 71 | +- **Tree-matching icons** — each pick carries the tree item's own `iconPath` (clusters, |
| 72 | + `symbol-folder` for folders, databases), so the picker looks like the tree. |
| 73 | +- **Host as a second line (`getDetail`)** — connections show their host(s) so two |
| 74 | + same-named connections are distinguishable; `matchOnDetail` makes it searchable. |
| 75 | +- **Uniform two-line rows** — every selectable row has a `detail` (folders → `Folder`, |
| 76 | + databases → `Database`, clusters → host/`Cluster`) to avoid a ragged mix of one- and |
| 77 | + two-line rows. |
| 78 | +- **Navigation** — after trying a bottom-anchored Back/Exit-with-grouping layout (modeled |
| 79 | + on the Azure credentials browser) it felt heavier to navigate, so the final form is the |
| 80 | + simplest: **Back pinned to the top** followed by a separator, **no Exit** (Esc cancels), |
| 81 | + **no group headers** — relying on the tree's natural folders-before-connections order. |
| 82 | + |
| 83 | +## Telemetry |
| 84 | + |
| 85 | +- `documentdb.pickTreeNode` — `source` (caller), `leafContextValue`, `outcome` |
| 86 | + (`picked` | `cancelled` | `empty`), measurements `stepCount` / `maxDepth`. Added so we can |
| 87 | + see whether/how the reusable picker is actually used across features. |
| 88 | +- `playground.connect` and `playground.connectOnRun` — outcome of the connect flows. |
| 89 | + |
| 90 | +## Testing |
| 91 | + |
| 92 | +- `pickTreeNode.test.ts` — drill-down, leaf return, exclusion of action/placeholder nodes, |
| 93 | + icon and detail passthrough, Back/empty/no-connections states, flat folders-first order, |
| 94 | + and cancellation. |
| 95 | +- `PlaygroundService.test.ts` — save-time transfer (untitled→file, Save As, ambiguity and |
| 96 | + size guards) and in-session restore (restore when credentials present, no restore when |
| 97 | + absent or after explicit removal). |
| 98 | +- Full Jest suite, lint, and TypeScript build are green. |
| 99 | + |
| 100 | +## Out of scope / explicitly not done |
| 101 | + |
| 102 | +- **No persisted binding store** (Memento) and **no in-file connection hint** — both |
| 103 | + rejected as too heavy or user-visible; the cross-session case is handled by the Connect |
| 104 | + affordance instead. |
| 105 | +- The picker does **not** auto-run anything on connect; it only restores/sets the binding. |
0 commit comments