diff --git a/AGENTS.md b/AGENTS.md index 38ddf783..48057ea3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,8 @@ SharpLsp is an open-source, editor-agnostic .NET LSP (C# + F#) built in Rust: one server, the complete .NET development experience in every editor. Stack: C#, Rust, TypeScript +All large files like videos MUST go in Git LFS + **Aim #1: Fix the .NET developer experience.** Match and surpass Visual Studio, Rider, and C# Dev Kit — no proprietary dependencies, licenses, or vendor lock-in. @@ -104,8 +106,9 @@ F# ahead of C# on every new feature. F# never takes the back seat. # Git -> ⛔️ **Don't touch git** unless the user asks or a skill demands it. - +> ⛔️ **Don't make git changes like creating new branches** unless the user asks or a skill demands it. +- Commit and push to submodules before commiting and pushing the feature branch +- Commit and push after each major change - **One feature branch**, never `git worktree`. Work the open one; several open → merge them immediately - **Never push to `main`.** PR → CI green → squash-merge → delete the branch - **Own every PR until green**: `gh pr merge --auto --squash`, then fix and push until all checks pass diff --git a/Makefile b/Makefile index 809dac3f..ef12acdc 100644 --- a/Makefile +++ b/Makefile @@ -795,7 +795,7 @@ _test-tooling: node --test tools/vsix/chunk-once.test.mjs @echo "==> Running repo tooling tests..." node --test tools/netcoredbg/custody.test.mjs tools/make/reinstall-loop.test.mjs tools/make/vsix-rebuild.test.mjs tools/vsix/rebuild-contract.test.mjs tools/vsix/debug-cleanup.test.mjs tools/audit/dotnet-vulnerable.test.mjs \ - tools/rider/gradle.test.mjs tools/vsix/resolve-symlink-stubs.test.mjs + tools/rider/gradle.test.mjs tools/vsix/resolve-symlink-stubs.test.mjs tools/make/vsix-profiles.test.mjs _website-build: @echo "==> Building website..." @@ -807,13 +807,13 @@ _website-dev: # [WEB-DESIGN-MEDIA] Shoot and publish one website film from the packaged VSIX: # `make _film FILM=fsharp`, or `PUBLISH=1` to publish the last shoot again. -# The films live in src/website/tools/profiler-film/films; its README says how. +# The films live in src/website/tools/video/films; its README says how. _film: @test -n "$(FILM)" || { echo "usage: make _film FILM= [PUBLISH=1]" >&2; exit 1; } $(if $(PUBLISH),,$(MAKE) _build-vsix VSIX_PREBUILT=1) @# Install once: npm ci would wipe the voice model kokoro-js caches in node_modules. - test -d src/website/tools/profiler-film/node_modules || npm ci --prefix src/website/tools/profiler-film - node src/website/tools/profiler-film/film.mjs $(FILM) $(if $(PUBLISH),--publish,) + test -d src/website/tools/video/node_modules || npm ci --prefix src/website/tools/video + node src/website/tools/video/film.mjs $(FILM) $(if $(PUBLISH),--publish,) _test-website: @echo "==> Running website Playwright tests..." @@ -839,6 +839,8 @@ _lint-zed: _lint-vsix: _check-vsix-chunks _check-sdk-pin _check-spec-citations # The profiler contributions are generated from the shared UI library ([PROFILER-SHARED-LIBRARY-VSCODE]). node tools/vsix/sync-profiler-contributions.mjs --check + # The tutorial's types are generated from its typeDiagram model ([TUTORIAL-MODEL]). + node tools/vsix/tutorial-model.mjs --check node --test tools/ci/security-gates.test.mjs tools/ci/changed-files.test.mjs tools/ci/vsix-coverage.test.mjs \ tools/ci/spec-citations.test.mjs tools/vsix/signals-bundle.test.mjs npm run lint:eslint --prefix $(VSCODE_DIR) @@ -1107,6 +1109,11 @@ _deploy-sidecars: # The extension identifier, read from the manifest that defines it. Hardcoding it # is how the previous uninstall target came to name an extension id that had not # existed for months, silently uninstalling nothing. [DIST-VSIX-DEV-INSTALL] +# The VS Code profiles, joined by "|", that hold their own copy of SharpLsp: the +# install loop replaces it in each, not only in the default profile. Override +# with VSCODE_PROFILES='A|B', or VSCODE_PROFILES= for the default profile only. +# [DIST-VSIX-DEV-INSTALL] +VSCODE_PROFILES ?= $(shell node tools/vsix/vscode-profiles.mjs $(EXTENSION_ID)) EXTENSION_ID = $(shell node -e "const p=require('./$(VSCODE_DIR)/package.json');process.stdout.write(p.publisher+'.'+p.name)") # The per-OS half of the install loop: where the VS Code CLI lives, and how stale @@ -1154,13 +1161,17 @@ RESOLVE_CODE = \ # SharpLsp loaded in VS Code pretending to be the change under test. # `_build-vsix` pulls in `_build-rust` and `_build-dotnet`, so the Rust host and # both sidecar binaries are rebuilt here, not reused from the cleaned tree. +# +# The profiles holding SharpLsp are read BEFORE the uninstall, which removes it +# from them, and handed to both ends of the loop. reinstall-vsix: @echo "==> Uninstall, full clean, rebuild and reinstall for $(HOST_PLATFORM)..." - $(MAKE) uninstall-vsix - $(MAKE) _kill - $(MAKE) _clean-artifacts - $(MAKE) _build-vsix - $(MAKE) install-vsix + @profiles='$(VSCODE_PROFILES)'; \ + $(MAKE) uninstall-vsix VSCODE_PROFILES="$$profiles" && \ + $(MAKE) _kill && \ + $(MAKE) _clean-artifacts && \ + $(MAKE) _build-vsix && \ + $(MAKE) install-vsix VSCODE_PROFILES="$$profiles" install-vsix: @test -f $(DEV_VSIX) || { \ @@ -1169,7 +1180,11 @@ install-vsix: } @echo "==> Installing $(DEV_VSIX)..." @$(RESOLVE_CODE); \ - "$$code_cli" --install-extension "$(DEV_VSIX)" --force + "$$code_cli" --install-extension "$(DEV_VSIX)" --force || exit 1; \ + list='$(VSCODE_PROFILES)'; IFS='|'; for profile in $$list; do \ + echo "==> ...and into VS Code profile '$$profile'"; \ + "$$code_cli" --install-extension "$(DEV_VSIX)" --force --profile "$$profile" || exit 1; \ + done @echo "==> $(EXTENSION_ID) installed. Reload VS Code to pick it up." # Uninstalling what is not installed is a success, not a failure: the reinstall @@ -1177,7 +1192,10 @@ install-vsix: uninstall-vsix: @echo "==> Uninstalling $(EXTENSION_ID)..." @$(RESOLVE_CODE); \ - "$$code_cli" --uninstall-extension "$(EXTENSION_ID)" || true + "$$code_cli" --uninstall-extension "$(EXTENSION_ID)" || true; \ + list='$(VSCODE_PROFILES)'; IFS='|'; for profile in $$list; do \ + "$$code_cli" --uninstall-extension "$(EXTENSION_ID)" --profile "$$profile" || true; \ + done # ── Install (private) ───────────────────────────────────────────── diff --git a/docs/models/vscode-tutorial.td b/docs/models/vscode-tutorial.td new file mode 100644 index 00000000..f0661049 --- /dev/null +++ b/docs/models/vscode-tutorial.td @@ -0,0 +1,40 @@ +typeDiagram + +# The interactive tutorial's model ([TUTORIAL-MODEL]). The extension's +# src/tutorial/model.generated.ts is generated from this file by +# tools/vsix/tutorial-model.mjs; never edit that file by hand. + +# A language the tutorial teaches. F# is taught first. +union TutorialLanguage { + FSharp + CSharp +} + +# One thing the user does in the live editor. +union TutorialAction { + # Open a source file of the language in an editor. + OpenFile + # Hover a symbol, and SharpLsp answers with Quick Info. + Hover + # Trigger completion, and SharpLsp answers with items. + Completion + # Go to a symbol's definition, and SharpLsp answers with a location. + Definition + # Find all references, and SharpLsp answers with locations. + References + # Invoke code actions (Cmd+. / Ctrl+.), and SharpLsp answers with fixes and refactorings. + CodeAction + # Show the Solution Explorer. + SolutionExplorer +} + +# A step of one language's walkthrough. +type TutorialStep { + language: TutorialLanguage + action: TutorialAction +} + +# Every step the user has carried out, persisted across sessions. +type TutorialProgress { + done: List +} diff --git a/docs/specs/DESIGN-SYSTEM.md b/docs/specs/DESIGN-SYSTEM.md index 0f70f2fe..9fc3b85d 100644 --- a/docs/specs/DESIGN-SYSTEM.md +++ b/docs/specs/DESIGN-SYSTEM.md @@ -126,7 +126,8 @@ Page-level grids and marketing card styles must not leak into `.prose`. Do not r Screenshots and video of a SharpLsp feature show the real extension, never a mock-up. -- **Capture.** Product films are shot by `src/website/tools/profiler-film/` (its README says how). Every shot runs on `kit/set.mjs`'s `onSet`: an isolated VS Code (its own user-data and extensions directories, never the user's editor) running the packaged VSIX against a real .NET demo from `demos/`. A walkthrough is one file in `films/`, a list of scenes that `film.mjs` shoots as real-time clips and publishes; `kit/acts/` tells the shared stories (the CPU and memory loops, the language features, the Solution Explorer), so each film supplies only its words, its demo and its edits. The profiler films run on the C# Storefront demo and its F# twin, FsStorefront; the language films run on `Shop.slnx`, both in one solution, and show F# before C#. Every F# claim the docs make is shown on F# code. Every feature page's still, and every figure in the launch post, is shot the same way by `films/doc-stills.mjs`; no page shows a capture from any other source. The composited one-minute film is `films/profiler/`: `capture.mjs` records stills and screencasts, and `render.mjs` turns them into the site's assets. +- **Capture.** Product films are shot by `src/website/tools/video/` (its README says how). Every shot runs on `kit/set.mjs`'s `onSet`: an isolated VS Code (its own user-data and extensions directories, never the user's editor) running the packaged VSIX against a real .NET demo from `demos/`. Each film is a folder of its own, `films//`, holding only its words, timings and edits (`film.mjs`, and `narration.json` when narrated); all shared code is in `kit/`. A walkthrough's `film.mjs` is a list of scenes that the entry point `film.mjs` shoots as real-time clips and publishes through the one film camera ([WEB-DESIGN-MEDIA-CAMERA]); `kit/acts/` tells the shared stories (the CPU and memory loops, the language features, the Solution Explorer), so each film supplies only its words, its demo and its edits. The profiler films run on the C# Storefront demo and its F# twin, FsStorefront; the language films run on `Shop.slnx`, both in one solution, and show F# before C#. Every F# claim the docs make is shown on F# code. Every feature page's still, and every figure in the launch post, is shot the same way by `films/doc-stills/`; no page shows a capture from any other source. The composited one-minute film, `films/profiler/`, goes through the same entry point: its `film.mjs` names its own shoot (`capture.mjs`, stills and screencasts) and its own publish (`render.mjs`, which steps `film.html` frame by frame into the site's assets). +- **Lessons.** A lesson teaches one feature end to end: a narrated slide film (`kit/lesson/`) that names the shortcut, explains where the feature's results come from, shows each step on real footage, shows the code before and after, explains what happens underneath, and recaps. Its footage is cut from the feature's published walkthrough, cropped clear of the walkthrough's step caption, so it never shows a mocked editor, and a fast-forwarded stretch keeps its speed badge. Slides are typography and diagrams only; code shown on a slide is text, never a drawing of the editor. Each slide holds until its lines are spoken, and each item appears as the line naming it starts. New lessons start from `templates/lesson/`; `films/refactoring-lesson/` is the reference. The docs page the lesson teaches leads with it, follows its steps in prose with code to try, and keeps the walkthrough as the one-take recording. - **Honesty.** Footage plays in real time unless the frame says otherwise: every fast-forwarded stretch carries an on-screen speed marker, and captions name time-lapses. Motion graphics (title cards, camera moves, highlights, the cursor) frame the footage but never alter what the editor shows. - **Privacy.** No frame may show a user name, home directory or hostname. The demo runs from a neutral temporary path, and the editor runs on a copy-on-write clone of the .NET install under that path, with its login-shell environment off, so every path it prints is neutral. A terminal appears only where the feature runs in one (F# Interactive, a build task, a contributor's command line), and a shell terminal starts with no profile and a bare `$ ` prompt. - **Formats.** @@ -136,10 +137,30 @@ Screenshots and video of a SharpLsp feature show the real extension, never a moc - Marketing pages may autoplay a film only when it is muted and looping; docs pages use controls and are never muted. - Every autoplaying video pauses and shows its controls under `prefers-reduced-motion: reduce`. - Every video ships a WebP poster, the films and the recordings alike. -- **Narration.** A walkthrough may be narrated (`kit/sound/`). Its reviewed script (`films/.narration.json`) is spoken by Kokoro-82M through `kokoro-js` on the capturing machine, never by a speech service or the operating system's voices, whose licences forbid publishing. Each line starts just after its scene's cut, and a scene holds its last frame until its line ends. The music is an original score synthesised from code (`kit/sound/score.mjs`), so no film carries licensed or sampled music, and it ducks under the voice. A narrated film ships AAC in its MP4 and Opus in its WebM, plays unmuted with controls, and carries WebVTT captions in every site language (`..vtt`, the page's own language shown by default), listed under `captions` in the catalogue. +- **Narration.** A walkthrough may be narrated (`kit/sound/`). Its reviewed script (`films//narration.json`) is spoken by Kokoro-82M through `kokoro-js` on the capturing machine, never by a speech service or the operating system's voices, whose licences forbid publishing. Each line starts just after its scene's cut, and a scene holds its last frame until its line ends. The music is an original score synthesised from code (`kit/sound/score.mjs` for walkthroughs, the quicker `kit/sound/drive.mjs` for lessons), so no film carries licensed or sampled music, and it ducks under the voice. A narrated film ships AAC in its MP4 and Opus in its WebM, plays unmuted with controls, and carries WebVTT captions in every site language (`..vtt`, the page's own language shown by default), listed under `captions` in the catalogue. - **One catalogue.** Every video on the site is declared once in `src/website/src/_data/videos.json`: encodings, size, length, upload date, the `page` that explains it, and its label and caption in each language. Every video also ships a 1200x630 JPEG social card, `-card.jpg`, cut from its poster frame by `kit/encode.mjs`. Pages place a video with the `{% video "" %}` shortcode (`src/website/video-figure.js`) and never repeat `