secondlife / secondlife/sl-vscode-plugin
Pull an Object into the Workspace
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 33
- Forks
- 15
- Avg merge
- 14h 50m
- Merged PRs (30d)
- 9
Description
Implementation Plan: Pull an Object into the Workspace
This document defines the design and implementation for copying the scripts and
notecards of a published in-world object into a local workspace folder, and linking
the resulting files to their in-world counterparts.
Tracking issue: split from secondlife/sl-vscode-plugin#130 ("pull" half).
Related documents:
Overview
Today the only way to get an object's contents into the workspace is to open each
item from the Second Life explorer and save it somewhere by hand. slVscodeEdit.autoLinkObject
solves the matching half of the problem for files that already exist; this feature
covers the case where they do not.
Pull walks a published object, writes one local file per script and notecard into
a folder named for the object, mirrors child prims as subfolders, and links every
resulting file to the in-world item it came from.
The operation is read-only with respect to the viewer. It never writes in-world.
Goals
- Produce a complete, predictable on-disk snapshot of an object's text content.
- Mirror the linkset structure so items from different prims never collide.
- Link every copied file to its originating item, exactly, with no heuristics.
- Never damage existing workspace files unless the user explicitly opts in.
- Confine all filesystem writes to a single destination folder.
- Report one summary at the end, with per-item detail in the log.
Non-Goals
- Writing anything in-world.
- Reconstructing
#include/requirestructure from flattened source. - Matching copied items against pre-existing workspace files (see
Design decision 1). - Persisting links across sessions.
- Deleting or pruning local files that no longer exist in-world.
- Reporting orphaned local files — files in the destination with no matching
in-world item. - Exporting non-text inventory (textures, sounds, animations, contained objects).
The protocol does not expose them.
Background
Two existing behaviours matter here.
Linking is item-driven and ephemeral. A link is created as a side effect of
opening an sl:// document, lives in ScriptSync.fileMappings keyed by
{ rootId, primId, itemId }, and is lost when the window closes. Nothing on disk
describes it. autoLinkObject re-establishes links in bulk by re-running the
matcher.
Matching is filename-based and ambiguous. SynchService.findMasterFile() tries,
in order: an @file metadata comment in the item's content, an exact
**/{name}.{ext} workspace search taking files[0], then a flattened-path
heuristic. With one object open this is usually right. Across several pulled
objects that each contain a trigger.luau, it is a coin toss.
Pull deliberately does not use the matcher. It knows the item_id → path mapping by
construction, because it created the files.
Design Decisions
These were settled during design review and are recorded so the rationale is not lost.
| # | Decision | Rationale |
|---|---|---|
| 1 | Do not attempt to find an existing master. Always write into the object folder. | findMasterFile answers "which file did the user mean when they opened one item?" Applying it to a bulk operation multiplies a recoverable guess into dozens of silent ones. Pulling window when door/trigger.luau already exists must not touch door. |
| 2 | Two modes: safe (skip existing files) and overwrite. Safe is the default. | Safe makes re-running the pull idempotent. Overwrite serves "refresh my local copy from in-world", which is a real workflow but must be a deliberate act. |
| 3 | Mode is chosen at invocation, not in settings. | A destructive mode reachable by a stale configuration value is a trap. |
| 4 | All writes are confined to the destination folder. | This is the entire safety story. It is an invariant, not a convention. |
| 5 | Prompt for the destination folder, pre-filled with the sanitised object name. | Solves three problems with one input box: multi-root workspaces, objects named Object colliding, and unnamed objects. |
| 6 | Child prim folders are always suffixed with the link number, not only on collision. | Conditional suffixing means adding a second prim named Door on a later pull renames the first one's folder and orphans its contents. |
| 7 | Content is written verbatim apart from stripping the emitted metadata block. | What is in-world may be preprocessor output. Pretending otherwise, or attempting to un-flatten it, would be a lie. The metadata block is stripped so pulled files do not claim to be a path they are not. |
| 8 | Link by construction after writing; never by matching. | The mapping is known exactly at write time. |
| 9 | Re-running a pull is additive. Items deleted in-world leave local files behind, untouched and unreported. | Deleting user files during an operation named "pull" would be a surprise. |
| 10 | Notecards keep their SL name verbatim; no synthetic .txt is appended. |
The inventory name is the filename. Appending an extension makes the local name disagree with the item name for no benefit, and push would have to undo it. |
| 11 | Always prompt for the workspace root, even when only one is open. | One consistent flow, and the destination is stated explicitly before anything is written. |
| 12 | No orphan reporting. | "Orphan" cannot be distinguished from "a file the user put there" without durable persistence, which is out of scope. |
| 13 | @line markers are preserved. |
They are not only preprocessor noise — they mark where required or included files were inlined, which is information the reader needs. |
Detailed Design
Command and surfaces
- Id:
slVscodeEdit.pullObjectToWorkspace - Title:
Second Life: Pull Object to Workspace - Scope: one published object, including the root prim and every linked prim.
Surfaces:
- Native tree context menu, on nodes with
contextValueofslObject. - Webview explorer object "more actions" menu.
- Command palette. When invoked without an argument, prompt with a quick pick of
published objects; if exactly one is published, use it without prompting.
Invocation flow
- Resolve the target object from
ObjectContentService.getInstance().getObject(objectId).
Abort with a message if it is not published. - Snapshot the object tree.
object.updatesends full-replacement arrays and can
land mid-run; all subsequent steps operate on the snapshot. - Prompt for the workspace root (quick pick, always shown, even when a single root is
open), then for the destination folder (input box, pre-filled with the sanitised
object name). - Prompt for the mode (quick pick: Safe — skip existing files / Overwrite existing
files). Safe is the default item. - If overwrite was chosen, enumerate which files would be overwritten and confirm with
a count (and names, when the count is small). Abort cleanly on cancel. - Run the export inside
vscode.window.withProgress,cancellable: true, reporting
prim / itemper step. - Show the summary.
Destination layout
<destination>/
<root item files>
<sanitised child name>_<link_number>/
<child item files>
- Root prim items go directly in the destination folder.
- One subfolder per child prim, named
${sanitise(link_name)}_${link_number}. - Child prims with empty inventories produce no folder.
Name sanitisation
SL names permit characters that filesystems do not. One shared helper handles both
prim folder names and item filenames:
- Replace
< > : " / \ | ? *and control characters0x00–0x1Fwith_. - Trim leading and trailing whitespace and trailing dots.
- Empty result becomes
unnamed. - Reserved Windows device names (
CON,PRN,AUX,NUL,COM1–COM9,
LPT1–LPT9), with or without an extension, are prefixed with_. - Truncate to a bounded length, preserving any extension.
- Resolve collisions within the same directory — including case-insensitive
collisions, since Windows and macOS filesystems do not distinguishDoorfrom
door— by appending_2,_3, … Duplicate item names within a single prim are
handled by the same rule.
The helper is pure and has no VS Code dependency, so it is unit-testable directly.
File extensions
- Scripts:
.lslor.luau, fromlanguageForItem(item)in
src/vscode/objectcontentprovider.ts. - Notecards: no synthetic extension.
displayName()already returns the name verbatim
and pull does the same, so the local filename equals the SL inventory name.
Content retrieval
Read through the existing virtual filesystem rather than calling the transport
directly:
const uri = itemUri(objectId, primId, item.item_id);
const content = Buffer.from(await vscode.workspace.fs.readFile(uri)).toString("utf-8");
This is what autoLinkObject already does. It reuses the provider's caching, error
mapping and notecard envelope stripping for free.
Before writing, strip the metadata block that ScriptSync emits on save (the
@file / creator comment lines — see the meta emission in
src/scriptsync.ts). Everything else is preserved verbatim,
including inlined include bodies and @line markers — the markers are kept
deliberately, because they show where required or included files were expanded.
Write rules
- Target path must resolve inside the destination folder. Enforce this after
sanitisation and normalisation; skip and log anything that escapes. - Safe mode: if the target exists, skip. Do not read it, do not compare it.
- Overwrite mode: overwrite, except when the file is open with unsaved changes
(vscode.workspace.textDocumentswithisDirty), in which case skip and report. - Items whose content cannot be fetched are reported as unreadable and their existing
local file, if any, is left untouched. Overwrite mode is therefore not a refresh
guarantee, and the summary must say so.
Linking
After a file is written — or skipped because it already exists — link it to the
originating item. The mapping is exact; no matching is performed.
This requires a new seam. SynchService.linkSlItem() currently resolves the master
itself by calling findMasterFile partway through
(src/synchservice.ts). Extract everything after resolution:
private async linkSlItemToMaster(
uri: vscode.Uri,
content: string,
masterUri: vscode.Uri,
options: { reveal: boolean },
viewerDocument?: vscode.TextDocument,
): Promise<SlLinkResult>
linkSlItem() becomes resolve-then-delegate, so the single-item open path is
unchanged byte for byte. Pull calls linkSlItemToMaster with reveal: false.
Existing behaviour that carries over unchanged:
- No-modify items are not linked. No-modify notecards are still readable and are
therefore copied but not linked; they must be reported as such. - A mismatch between the local file and the in-world content is counted, not prompted.
Bulk prompting is explicitly rejected (plan-autolink-object-files.md, decision 2).
Summary
One notification, with the detail written to the plugin log:
| Category | Meaning |
|---|---|
written |
New file created. |
overwritten |
Existing file replaced (overwrite mode). |
skipped (exists) |
Target present, safe mode. |
skipped (open and modified) |
Dirty editor buffer, overwrite mode. |
unreadable |
Permissions or fetch failure; nothing written. |
linked |
Linked to the in-world item. |
copied, not linked |
No-modify item. |
differing |
Linked, but local content differs from in-world. |
failed |
Unexpected error; see log. |
The summary should also state plainly that pulled script content is what is stored
in-world, which for scripts previously pushed by this extension is preprocessor
output rather than the original modules.
Configuration
None. The mode is chosen at invocation (decision 3) and notecard names are used
verbatim (decision 10), so this feature contributes no new settings.
Error handling
| Condition | Behaviour |
|---|---|
| Object not published | Abort before any filesystem work, with a clear message. |
| No workspace open | Abort with a message. |
| Object unpublished or deleted mid-run | Stop, report a partial result. |
| Cancellation | Stop after the current item; already-written files remain; report partial. |
| Destination folder cannot be created | Abort before writing anything. |
| Individual item fetch failure or timeout | Count as unreadable, continue. |
Implementation Phases
Each phase is independently shippable and independently reviewable.
Phase 0 — Shared helpers
- Add a path-safety module exporting
sanitiseSegment(name)and
uniqueInDirectory(name, taken), implementing the rules in
Name sanitisation. Pure functions, no VS Code imports. - Add a
stripEmittedMeta(content, language)helper that removes the metadata block
ScriptSyncemits, reusing the same comment-prefix resolution as
findMasterFileByMetaComment. - Unit tests for both, covering reserved names, control characters, trailing dots,
empty names, case-insensitive collisions and truncation.
Phase 1 — Export core, safe mode, no linking
- Add
SynchService.pullObjectToWorkspace(objectId, options)modelled on
autoLinkObject: snapshot, flatten root plus linked prims into a work list,
withProgressloop with cancellation, accumulate a summary. - Implement destination resolution — the always-shown workspace root quick pick,
followed by the pre-filled folder input box. - Implement the layout: root items at the destination,
name_linknumbersubfolders
for children, empty-inventory prims skipped. - Implement fetch → strip metadata → write, with the containment check and the safe
mode skip rule. - Implement the summary notification and log detail.
- Register the command in src/extension.ts alongside
slVscodeEdit.autoLinkObject, and contribute it inpackage.jsonfor the command
palette andview/item/contextonslObject.
Phase 2 — Exact linking
- Extract
linkSlItemToMasterfromlinkSlItemwith no behavioural change; confirm
the existing open-a-single-item path still behaves identically. - Call it for every written and skipped-because-exists file, with
reveal: false. - Count
linked,copied, not linkedanddifferingin the summary. - Refresh the synced-file decorator once at the end rather than per file.
Phase 3 — Overwrite mode and guardrails
- Add the mode quick pick.
- Add the pre-flight enumeration and confirmation for overwrite.
- Add the dirty-buffer skip.
- Distinguish
overwrittenfromunreadablein the summary so a partial refresh is
never mistaken for a complete one.
Phase 4 — Surfaces and polish
- Add the action to the webview object menu in
src/webview/explorer/explorer.ts and handle
the message in src/vscode/objectexplorerwebview.ts,
following theautoLinkObjectprecedent. - Update features-overview.md and
USER_GUIDE.md.
Testing
Unit — sanitisation and metadata stripping, exhaustively. These are pure and cheap
to cover.
Integration, with a stubbed ObjectContentService and an in-memory or temp
workspace:
- Root-only object; root plus multiple children.
- Two children with identical names; two items with identical names in one prim.
- Names containing
/,:,*, trailing dots, and a reserved device name. - Empty child inventory produces no folder.
- Safe mode leaves existing file content byte-identical.
- Overwrite mode replaces content, and skips a dirty buffer.
- Unreadable item is counted and leaves no file.
- Cancellation mid-run yields a partial result with files already written intact.
- A path that would escape the destination after sanitisation is rejected.
Manual, against a viewer:
- No-copy and no-modify scripts are reported, not silently missing.
- A no-modify notecard is copied but not linked.
- After pull, saving a pulled file pushes to the correct in-world item.
- A linkset change during a pull does not corrupt the traversal.
Future Work
Out of scope here, recorded so the reasoning is not lost:
- Explicit re-link. Bind a chosen workspace file to a specific in-world item,
bypassingfindMasterFile. Pull manufactures many same-named files across object
folders, and session links are ephemeral, so on the next sessionautoLinkObject
has to guess between them. Likely surfaces as "Link to File…" on an explorer item
and "Link to In-World Item…" on a workspace file. - Durable link persistence — a manifest recording
item_id→ relative path. The
natural companion to re-link, and a prerequisite for the push feature. - Backup-then-overwrite, and a "show me the differences" mode that opens diffs
instead of writing. - Selective pull — choose a subset of items rather than all-or-nothing.
- Orphan reporting — distinguishing a stale pulled file from one the user created
requires the durable persistence described above.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the Detailed Design, then read src/vscode/objectcontentprovider.ts, src/scriptsync.ts, and src/synchservice.ts, along with the pullObjectToWorkspace command entry point. Identify the pure name-sanitisation helper and its unit tests first. Done means the command supports the specified surfaces, snapshot and safe/overwrite flow, writes and links the object tree, and reports the listed outcomes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, vscode
- Domain
- devtools, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100