Skip to content
| Marketplace
Sign in
Visual Studio Code>Other>sp-keshavarzNew to Visual Studio Code? Get it now.
sp-keshavarz

sp-keshavarz

Noctyra

|
39 installs
| (1) | Free
Vscode extension for work
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

sp-keshavarz

A code-generation extension for people who write the same shapes every day.

It scaffolds whole folders from a template, completes single lines from a prefix and adds the imports they need, builds a form out of a list of field names, turns a sample API response into a TypeScript type, and copies a folder you already wrote under a new name — rewriting every casing of it on the way.

Everything it emits comes from a template you can read and replace. It is fully offline: no network calls, no AI, no runtime dependencies.

It adapts to the project it is in. Import paths are resolved against that project's own tsconfig aliases, so it writes @/... in one repo and #shared/... in another without being configured. Anything that would not make sense in your project is not offered at all.

At a glance

Key What it does
Generate Ctrl+Alt+N A folder of files from a recipe, imports wired up, cursor on the first thing to type
Snips Ctrl+Space A prefix and Tab that inserts a line and its import
Form fields Ctrl+Alt+F A list of names becomes a form's inputs, schema, empty values and type
Type from JSON Ctrl+Alt+J A sample response becomes an export type
Duplicate As... right-click a folder A copy under a new name, every casing rewritten

On macOS all of these are Cmd instead of Ctrl.

Install

From the marketplace, or with a .vsix:

code --install-extension sp-keshavarz-0.8.0.vsix

Reload the window and it is on. Nothing needs configuring — but see Making it yours, because the templates that ship with it are shaped around one set of conventions and yours are probably different.


Generating

Press Ctrl+Alt+N, or right-click a folder in the explorer and choose My Workflow. Pick a generator, type a camelCase name, and the files appear. The main one opens with its placeholders live: Tab moves between the things you actually have to fill in.

What ships is four generators shaped around a React/React-Query codebase — a component, an icon, a query and a mutation. The point is not that you use those; it is that adding your own means adding a folder.

It is a single WorkspaceEdit, so Ctrl+Z undoes a whole generation.

It works out where the files go. A recipe can name a destination relative to the package root and read part of it from the project — so generating a query from anywhere inside modules/finance writes to modules/finance/queries whether or not you navigated there, and without asking, because the module is implied by where you invoked it. A folder you actually clicked in the explorer is always obeyed instead.

To bind a key straight to one generator, skipping the picker:

{ "key": "ctrl+alt+q", "command": "sp-keshavarz.generate", "args": { "recipe": "query" } }

The argument is the recipe's folder name, including recipes you add yourself.


Snips

Type a prefix, press Tab, and the code appears — the way a normal VS Code snippet works, except the import it needs is added at the top of the file in the same keystroke:

qr⇥   const { data, isPending } = useQuery(getUserQueryOptions());
      + import { useQuery } from "@tanstack/react-query";

Nothing is added twice: a file that already imports useQuery gets no new line, and a file that imports something else from @tanstack/react-query has that import widened in place rather than imported again. The edit rides along as a completion's additionalTextEdits — the same mechanism TypeScript's own auto-import uses — so it never disturbs the snippet you are tabbing through.

A snip is only offered where it applies

This is what makes it safe to leave switched on in every project you open:

  • The library has to be installed. qr needs @tanstack/react-query in the package's dependencies; bx needs @mui/material; frm needs react-hook-form. In a project without them they are not in the list, and in a plain Node project you are left with the handful that are true anywhere.
  • The file has to exist. A snip that imports something from inside the project — a Text component, a useModal hook — is offered only where that file is actually there. Inserting a reference to something a project does not have is worse than offering nothing.

Two snips can also share a prefix and take turns, so one keystroke means the same thing in projects that do it differently: nv is useNavigate in a react-router project and the navigate hook in a Next one; frm brings a yup resolver where one is installed and plain useForm where it is not.

What ships

Anywhere clg console.log · tc try/catch · ty export type · enm enum · af async arrow fn
React us useState · uef useEffect · memo useMemo · ucb useCallback · uref useRef · red useReducer · uid useId · ctx createContext + hook · frag fragment
React Query qr useQuery · qo queryOptions · iq useInfiniteQuery · mut useMutation · qc useQueryClient · inv invalidateQueries · sqd setQueryData
Forms frm useForm · sub handleSubmit · watch useWatch · ctrl Controller · fa useFieldArray · fctx useFormContext · sch yup schema
Intl intl useIntl · fm a formatMessage call · fmsg <FormattedMessage /> · fnum <FormattedNumber />
MUI bx Box · bxf flex row Box · bxc flex column Box · stk Stack · ib IconButton · cp CircularProgress · thm useTheme · sxp an sx prop
Styling (sx) fx flex row · fcol column · fbtw space-between · fctr centered · fwrap wrap · grid grid columns · abs absolute · absc absolute centered · sq square · full fill parent · cover image cover · ell ellipsis · ptr pointer · card bordered · bgx filled
Routing nv navigate · prm useParams · srch useSearchParams · pth usePathname · rtr useRouter
State zst a zustand store

The styling ones insert just the attribute, so they work on anything that takes an sx prop:

fx⇥    sx={{ display: "flex", alignItems: "center", gap: ".5rem" }}
card⇥  sx={{ p: "1rem", borderRadius: ".75rem", border: "1px solid", borderColor: "grey.300" }}

A few more are shaped around components a particular codebase has (txt, fb, fbi, mdl, tst, hn). They appear only where those files exist, which in your project probably means never — write your own instead.

My Workflow: Insert Snip in the command palette lists the ones that apply where you are, with what each would import.


A whole form from a list of names

A fifteen-field form is not one template, it is the same few lines fifteen times, and the only thing that changes is a name you already know. Press Ctrl+Alt+F and type the names (or select them in the editor first):

firstName, lastName, nationalCode, birthDate:date, gender:select:12, note?

Then pick which blocks to write — the inputs, a yup schema, an empty-values object, the FormValues type.

name:kind:size is the whole syntax. The size is grid columns and defaults to 6; a trailing ? makes a field optional. The kind is whatever your input component calls it and is passed through verbatim — what it means is inferred from the word, so priceInput is a number, uploadFile is a file and multiSelect is an array without any of them being configured. Commas, spaces and newlines all separate, so a column of names pasted out of a design or an API payload works as it is.

Which blocks you are offered follows the same rule as the snips: the yup schema where yup is installed, the inputs where a matching component exists. They are templates you can replace.


Type from JSON

Writing the response type is the expensive half of adding an endpoint: the call is four lines, the type is forty. Copy a sample response, put the cursor where the type belongs, and press Ctrl+Alt+J.

It reads the JSON from your selection (so JSON already pasted into the file is replaced by its type in place), otherwise from the clipboard, otherwise it asks.

Inference is structural and merges every sample it can see: a list where one row carries an extra field makes that field optional, and a value that is sometimes null becomes string | null. Keys are emitted exactly as the API sends them, quoted only when they are not valid identifiers. Empty arrays become unknown[] and an always-null field becomes null — both are signs the sample did not say enough, and are yours to fix.

You do not have to tidy the JSON first: trailing commas and comments are tolerated, and so is a fragment copied out of a larger body ("response": {…}), which is retried as the body of an object.

A { response: … } or { results: … } wrapper is unwrapped, so what you get is the type that goes inside your envelope rather than the envelope itself.


Duplicate As...

For anything more specific than a generator, the most accurate template is a folder you already wrote. Right-click one and choose Duplicate As...

It works out what to rename by scoring every plausible name against the folder's own file names and keeping the one that accounts for most of them — a getWalletQuery folder holding getWallet.* is renaming getWallet, and an activateUserMutation folder holding activateUser.* plus useActivateUser.ts is renaming activateUser, though those files share no common prefix. The prompt tells you which name it settled on before anything happens.

Type the new name and the copy appears beside it with every casing rewritten — getWallet, GetWallet, GET_WALLET, get-wallet, get_wallet — through file contents, file names, nested folders and import paths alike. Binary files are copied untouched. In a folder whose files have nothing in common (utils, locales, assets/icons) only the folder itself is renamed.

There is no dry-run mode because the whole duplicate is one Ctrl+Z.

Two things it deliberately leaves alone, because it cannot know the answer: URLs and query keys inside the copied code. Renaming is also substring-based, so an identifier that merely contains the stem is rewritten too — which is what makes the surrounding type and folder names come along.


Making it yours

The templates that ship are examples. Anything in a project's own .vscode/snips/ folder is read first, so a file with the same name replaces the bundled one, and anything new is simply added:

.vscode/
  snips/
    qr.snip              a prefix of your own, or a replacement for one of ours
    forms/
      inputs.snip        the shape your form inputs actually have
  recipes/
    endpoint/
      recipe.json        a generator of your own
      function.txt

Nothing has to be rebuilt or reloaded: files are re-read when they change, so editing a template takes effect on the next keystroke.

Setting Default
sp-keshavarz.snips.enabled true Offer snips as completions at all
sp-keshavarz.snips.bundled true Include the ones that ship. Turn off to use only your own
sp-keshavarz.snips.folders [".vscode/snips"] Workspace-relative folders to read from. Recipes come from a sibling recipes/, form blocks from a forms/ subfolder
sp-keshavarz.apiClient [] Package-root-relative paths to your API client, for <<import apiClient>>. Empty means try the built-in guesses
sp-keshavarz.responseTypes [] The same, for your shared response types

Template syntax

Recipes, snips and form blocks all use one marker syntax, <<...>>:

Marker Meaning
<<name>> The name you typed, verbatim
<<pascal name>> The name through a case filter
<<1>> A tab stop — where the cursor lands
<<1:text>> A tab stop with a default. Repeating an index mirrors it
<<0>> The final cursor position
<<#if flag>> … <<#else>> … <</if>> A conditional block
<<import types>> The import specifier for another file in the same recipe

Filters are camel, pascal, kebab, snake, constant, lower and upper. They work on any input casing, so <<kebab name>> turns getUserProfile into get-user-profile, and they work in file and folder paths too.

<<>> was chosen over {{}} because {{ collides with real code — JSX spreads and sx={{ … }} — which these templates are full of.

An unknown <<marker>> is left in the output verbatim rather than dropped, so a typo is visible immediately.

Writing a recipe

A recipe is a folder containing recipe.json and its templates. There is no TypeScript to change and no list to register it in.

{
  "label": "$(symbol-method) Component",
  "detail": "component.tsx + .types.ts",
  "noun": "Component",
  "placeholder": "myComponent",
  "example": "myComponent, userCard",
  "order": 10,
  "hints": ["components"],
  "folder": "<<name>>",
  "files": [
    { "id": "component", "path": "<<name>>.tsx", "template": "component.tsx.txt", "primary": true },
    { "id": "types", "path": "<<name>>.types.ts", "template": "types.txt" }
  ]
}
Field Meaning
label / detail How it reads in the picker ($(icon) codicons work)
noun Used in prompts and messages
placeholder / example Shown while you type the name
order Position in the picker; lower is higher up
hints Folder names that float this recipe to the top when you right-click one
anchor Folder names (or paths) searched upwards when there is no clicked folder; defaults to hints
prompts Values chosen before generating, read out of the project
target Package-root-relative folder to generate into, instead of the clicked one
folder Templated subfolder to create, or "." to write in place
options Yes/no toggles, asked before generating, readable as <<#if flag>>
files[].id How other templates refer to this file: <<import id>>
files[].path Templated file name, relative to folder
files[].template Template file, resolved inside the recipe's own folder
files[].primary The file that opens with its tab stops live
files[].when Only write this file when the named option is on

Choosing where it goes. A recipe can find its own destination instead of making you navigate there first:

"hints": ["queries"],
"prompts": [{ "name": "module", "label": "Which module?", "dirs": "src/modules" }],
"target": "src/modules/<<module>>/queries"

A prompt's choices are read out of the project as it is right now, never from a list kept in sync by hand:

Source Choices
"dirs": "src/modules" Subdirectory names of that package-root-relative folder
"exports": "src/types/api.ts" Names exported from that file
"values": ["a", "b"] A fixed list

A dirs prompt answers itself when the folder you invoked from is already inside one of the choices — generating from anywhere under modules/finance means the module is finance, with nothing to pick. "alwaysAsk": true opts out. When a recipe would redirect you somewhere else and the value could not be inferred, the chooser offers Here first, so it can never trap you into generating away from the folder you clicked.

Keyboard invocation. A folder you picked in the explorer is an instruction and is always obeyed. A folder that merely happens to contain the file you have open is not — so from a keybinding, a recipe without a target searches upwards for the folder its output belongs in, using anchor. Generating a component while editing pages/dashboard/dashboard.tsx puts it in the module's components folder, not inside the page. The nearest match wins.

A recipe that fails to parse is skipped with an explanation in the My Workflow output channel, rather than breaking the others.

Writing a snip

One file per prefix, in .vscode/snips/. A short header, ---, then the code exactly as it should appear:

prefix: qr
label: useQuery with an options builder
needs: package @tanstack/react-query
import: useQuery from @tanstack/react-query
---
const { data<<1>>, isPending } = useQuery(<<2>>QueryOptions(<<3>>));
Header Meaning
prefix What you type. The only required field
label / detail How it reads in the completion list
order Position wherever it is listed; lower comes first
import useState, useEffect from react — added when the snip is inserted
import (project file) ~components/kit/text/text finds that file in this project and writes whatever alias covers it. a \| b lists fallbacks. A snip whose project import is missing is not offered at all
import? The same, but optional: if it is missing the snip still appears without that import, and <<#if name>> is false
import (auto) auto Text from ~... reads the file and decides whether to import it named or default
import (default / type) default FormBuilder from ~..., type FieldValues from react-hook-form
needs package react-router-dom, or a ~path. Prefix with ! to invert it, which is how two snips share a prefix
have have: intl = package react-intl — not a gate, just a flag the body reads with <<#if intl>>
lang Any of ts, tsx, js, jsx. Defaults to all four

<<name>> in a snip body is the open file's own name, so <<pascal name>> gives you its component name.

Writing a form block

The blocks Ctrl+Alt+F writes are templates in a forms/ subfolder, in the same format plus a <<#each>> that repeats over the fields:

prefix: schema
label: yup schema
order: 30
needs: package yup
import?: requiredText from ~helpers/requiredText
---
const schema = object().shape({
<<#each>>
  <<key>>: <<yup>><<#if optional>>.optional()<<#else>>.required(<<#if requiredText>>requiredText<</if>>)<</if>>,
<</each>>
});

Inside <<#each>> each field offers <<name>>, <<key>> (quoted if it has to be), <<kind>>, <<size>>, <<yup>>, <<empty>>, <<type>> and <<group>>, with <<#if optional>> and <<#if requireable>>. Outside it there is <<typeName>> and <<count>>. The yup import is worked out from the factories your rendered block actually uses.

How imports are resolved

When the extension writes an import, it walks up from the target folder to the first tsconfig.json whose paths actually cover it, and writes the specifier against that alias — @/... in one project, #shared/... in another. It follows extends chains and references, so a solution-style tsconfig.json that only points at tsconfig.app.json resolves correctly, and in a monorepo it lands on the owning app or package rather than the repo root.

If no alias covers the folder, imports fall back to relative paths and a warning says why. Specifiers never carry a .ts/.tsx extension, so the output is valid whether or not a project enables allowImportingTsExtensions.

Whether a name arrives as { Text } or Text is read off the file it resolved to, so the same snip is correct in a project that exports it either way.

When something looks wrong

Open the My Workflow output channel. It logs which tsconfig and alias each run used, which prompt values were inferred, which snips were skipped and why, and any template that failed to parse. It is the first place to look when an import comes out in a shape you did not expect.


Development

pnpm install
pnpm run compile    # or: pnpm run watch
pnpm run lint

Press F5 for an Extension Development Host with the extension loaded. To run it against your day-to-day editor instead, symlink the repo into your extensions folder and reload:

ln -s "$PWD" ~/.vscode/extensions/noctyra.sp-keshavarz-dev

Requires VS Code 1.74+.

License

MIT.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
  • Your Privacy Choices
  • Consumer Health Privacy
© 2026 Microsoft