Snippets and tasks
Snippets and tasks travel with the VS Code profile through Settings Sync. The scripts that the tasks run do not: they live in ~/.vscode-scripts/ and have to be copied to each machine by hand (see Profiles and Settings Sync).
Snippets
Type the prefix in a Quarto document and press Tab (or pick it from the suggestion list).
All snippets are for Quarto (.qmd) files, most of them for reveal.js slides, which I use for teaching and talks.
Layout and structure (quarto.json)
quarto.json is a language snippet file: VS Code applies it to the quarto language only, because of its name.
| Prefix | Name | Inserts | Use |
|---|---|---|---|
br |
Quarto empty line | <br> |
Vertical space on a slide. |
qdiv |
Quarto Div | ::: … ::: |
An empty div, ready for a class. |
qcolumns |
Quarto Two Column Layout | :::: {.columns} with two ::: {.column width="50%"} |
Two columns; both widths are placeholders (Tab between them). |
qcol |
Quarto Column Layout | one ::: {.column width="50%"} |
Add another column inside an existing .columns. |
qlist |
Quarto Incremental list | ::: {.incremental} with a - item |
Bullets that appear one by one. |
qfrag |
Quarto Fragment | ::: {.fragment} |
Any block that appears on the next click. |
qquote |
Quarto Blockquote | ::: {.blockquote} |
A styled quote / call-out block. |
qfooter |
Quarto footer | ::: {.footer} |
Slide footer. |
qcomment |
Quarto comment | <!-- --> |
An HTML comment (not rendered). |
Teaching slides (quarto.code-snippets)
quarto.code-snippets is a global snippet file; each snippet sets "scope": "quarto" itself.
| Prefix | Name | Inserts | Use |
|---|---|---|---|
qexec |
Practical Exercise | A ## Practical Exercise slide (classes .exercise .text-center), a numbered blockquote, and r countdown(minutes = 20) |
Exercise slide with a 20-minute timer. Needs the countdown R package and slide CSS with the text-size-heading2, exercise classes. |
qdisc |
Discussion | The same with .discussion and a 5-minute timer |
Discussion slide. |
The files
Show the full file: R/snippets/quarto.json
{
// Place your snippets for quarto here. Each snippet is defined under a snippet name and has a prefix, body and
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
// same ids are connected.
// Example:
// "Print to console": {
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Quarto empty line": {
"prefix": "br",
"body": [
"<br>",
"$0"
],
"description": "Insert an empy line in Quarto documents"
},
"Quarto Div": {
"prefix": "qdiv",
"body": [
":::",
"$0",
":::"
],
"description": "Insert an empy div in Quarto documents"
},
"Quarto Two Column Layout": {
"prefix": "qcolumns",
"body": [
":::: {.columns}",
"",
"::: {.column width=\"${1:50}%\"}",
"$0",
":::",
"",
"::: {.column width=\"${2:50}%\"}",
":::",
"",
"::::"
],
"description": "Insert a two-column layout for Quarto documents"
},
"Quarto Column Layout": {
"prefix": "qcol",
"body": [
"::: {.column width=\"${1:50}%\"}",
"$0",
":::"
],
"description": "Insert a single-column for Quarto documents"
},
"Quarto Incremental list": {
"prefix": "qlist",
"body": [
"::: {.incremental}",
"",
"- $0",
"",
":::"
],
"description": "Insert an incremental list for Quarto documents"
},
"Quarto Fragment": {
"prefix": "qfrag",
"body": [
"::: {.fragment}",
"$0",
":::"
],
"description": "Insert a div with fragment class for Quarto documents"
},
"Quarto Blockquote": {
"prefix": "qquote",
"body": [
"::: {.blockquote}",
"$0",
":::"
],
"description": "Insert a blockquote in Quarto documents"
},
"Quarto footer": {
"prefix": "qfooter",
"body": [
"::: {.footer}",
"$0",
":::"
],
"description": "Insert a footer in Quarto documents"
},
"Quarto comment": {
"prefix": "qcomment",
"body": [
"<!-- $0 -->"
],
"description": "Insert a comment in Quarto documents"
}
}
Show the full file: R/snippets/quarto.code-snippets
{
// Place your global snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Practical Exercise": {
"scope": "quarto",
"prefix": "qexec",
"body": [
"## [Practical Exercise]{.text-size-heading2} {.exercise .text-center}",
"",
"<br>",
"",
"::: {.blockquote}",
"1. $0",
":::",
"",
"`r countdown(minutes = 20)`",
"",
],
"description": "Insert slide for practical exercise with countdown timer"
},
"Discusssion": {
"scope": "quarto",
"prefix": "qdisc",
"body": [
"## [Discussion]{.text-size-heading2} {.discussion}",
"",
"<br>",
"",
"::: {.blockquote}",
"1. $0",
":::",
"",
"`r countdown(minutes = 5)`",
"",
],
"description": "Insert slide for discussion with countdown timer"
}
}To install them by hand: Command Palette → Snippets: Configure Snippets → pick quarto (for quarto.json) or New Global Snippets file (for quarto.code-snippets), then paste the content. On disk they live in the profile’s snippets/ folder (%APPDATA%\Code\User\profiles\<profile id>\snippets\).
Tasks
Run a task with Command Palette → Tasks: Run Task, or with its keybinding.
| Task | Keybinding | Script | What it does |
|---|---|---|---|
Git: Return to updated main |
Ctrl+Alt+Shift+M |
return-to-main.ps1 |
After a PR is merged, gets you onto an up-to-date main with one checkout (details below). |
R: New scratch project |
none | new-r-scratch-project.ps1 |
Asks for a name, creates an RStudio-style R project in Documents\R-scratch\<name> with usethis::create_project(), and opens it in a new VS Code window. |
Both are "type": "process" tasks that start pwsh -NoProfile -File "${env:USERPROFILE}/.vscode-scripts/<script>.ps1". Using ${env:USERPROFILE} keeps tasks.json free of user names, so the file works on any machine.
Git: Return to updated main
The usual way back after a large PR is merged (git switch main, then git pull) updates the working tree twice: first to the old main, then to the new one. On a big repository both steps are slow.
The script avoids the first one:
- Stops if there are uncommitted changes.
- Finds the remote’s default branch (
main,master, …). - Fetches only that branch (
git fetch --no-tags origin main). - Moves the local
mainto the fetched commit without checking it out (git update-ref, only if it is a fast-forward). - Checks out
mainonce, straight into the new state. - Deletes the branch you came from with
git branch -d, which only succeeds when git sees it as merged intomainor identical to what was pushed. Otherwise it tells you and keeps the branch.
If you are already on main, it just fast-forwards (git merge --ff-only).
R: New scratch project
For quick experiments that deserve their own folder but not a repository.
Rscriptis taken fromPATH, i.e. rig’s default R version (rig default <version>). Pass-RScriptPathto use another one.- Needs the
usethispackage in the default R version (it comes withdevtools). - The task input
rScratchProjectNameis apromptString: VS Code asks for the name when the task starts.
The files
Show the full file: R/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Git: Return to updated main",
"type": "process",
"command": "pwsh",
"args": [
"-NoProfile",
"-File",
"${env:USERPROFILE}/.vscode-scripts/return-to-main.ps1"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "shared",
"clear": true
}
},
{
"label": "R: New scratch project",
"type": "process",
"command": "pwsh",
"args": [
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"${env:USERPROFILE}/.vscode-scripts/new-r-scratch-project.ps1",
"-Name",
"${input:rScratchProjectName}"
],
"problemMatcher": [],
"presentation": {
"reveal": "silent",
"panel": "shared",
"clear": true,
"echo": false,
"focus": false,
"showReuseMessage": false
}
}
],
"inputs": [
{
"id": "rScratchProjectName",
"type": "promptString",
"description": "Name for the new R scratch project"
}
]
}
Show the full file: vscode-scripts/return-to-main.ps1
# Return to an up-to-date default branch after a PR was merged, as fast as
# possible. Used by the VS Code task "Git: Return to updated main".
#
# Why it is fast: the usual "switch to main, then pull" updates the working
# tree twice (old main, then new main) - slow after a large PR. Here the local
# default branch is first moved to the fetched commit WITHOUT checking it out,
# then checked out once.
#
# Safe: stops on uncommitted changes; only fast-forwards; deletes the branch
# you left only when git confirms it is merged into main or matches what was
# pushed (git branch -d), otherwise it just tells you.
[CmdletBinding()]
param(
[string] $Remote = 'origin'
)
$ErrorActionPreference = 'Stop'
function Invoke-Git {
& git @args
if ($LASTEXITCODE -ne 0) { throw "git $($args -join ' ') failed (exit $LASTEXITCODE)." }
}
if (-not (git rev-parse --is-inside-work-tree 2>$null)) {
throw 'Not inside a git repository.'
}
if (git status --porcelain --untracked-files=no) {
throw 'There are uncommitted changes. Commit or stash them first.'
}
# Default branch of the remote (main, master, ...); fall back to "main".
$defaultRef = git symbolic-ref --quiet --short "refs/remotes/$Remote/HEAD" 2>$null
$default = if ($defaultRef) { $defaultRef -replace "^$Remote/", '' } else { 'main' }
$current = git branch --show-current
# Fetch only the default branch: the fastest fetch. (Other remote-tracking
# branches are not pruned here; run "git fetch --prune" for that.)
Write-Host "Fetching $Remote/$default..."
Invoke-Git fetch --no-tags $Remote $default
if ($current -eq $default) {
Invoke-Git merge --ff-only "$Remote/$default"
} else {
# Move the local default branch to the fetched commit without a checkout.
# (Creates it if it does not exist locally yet.)
$localExists = git show-ref --verify --quiet "refs/heads/$default"; $localExists = ($LASTEXITCODE -eq 0)
if ($localExists) {
$isAncestor = git merge-base --is-ancestor $default "$Remote/$default"; $isAncestor = ($LASTEXITCODE -eq 0)
if (-not $isAncestor) {
throw "Local '$default' has commits that are not on $Remote/$default. Resolve that by hand."
}
Invoke-Git update-ref "refs/heads/$default" "$Remote/$default"
} else {
Invoke-Git branch --track $default "$Remote/$default"
}
Write-Host "Switching to $default..."
Invoke-Git switch $default
# Tidy up the branch we came from, only if git agrees it is merged.
if ($current) {
git branch -d $current 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host "Deleted merged branch '$current'."
} else {
$upstreamGone = git for-each-ref --format='%(upstream:track)' "refs/heads/$current"
if ($upstreamGone -eq '[gone]') {
Write-Host "Branch '$current' was deleted on $Remote (probably squash-merged). Delete it with: git branch -D $current"
} else {
Write-Host "Kept branch '$current' (not merged into $default)."
}
}
}
}
Write-Host "Done: $default is at $(git log -1 --format='%h %s')."
Show the full file: vscode-scripts/new-r-scratch-project.ps1
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $Name,
# Default: the Rscript on PATH, i.e. rig's default R version
# (`rig default <version>`). Pass a full path to use another one.
[string] $RScriptPath = 'Rscript'
)
$ErrorActionPreference = 'Stop'
$projectName = $Name.Trim()
if ([string]::IsNullOrWhiteSpace($projectName)) {
throw 'The project name cannot be empty.'
}
if ($projectName -in '.', '..') {
throw "The project name '$projectName' is not allowed."
}
if ($projectName.IndexOfAny([System.IO.Path]::GetInvalidFileNameChars()) -ge 0) {
throw "The project name '$projectName' contains a character that is not allowed in a Windows folder name."
}
if ($projectName.EndsWith('.') -or $projectName.EndsWith(' ')) {
throw 'The project name cannot end with a period or a space.'
}
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($projectName)
if ($baseName -match '^(?i:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$') {
throw "The project name '$projectName' is reserved by Windows."
}
$rscriptCommand = Get-Command $RScriptPath -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $rscriptCommand) {
throw "Rscript was not found ('$RScriptPath'). Install R with rig and check that 'C:\Program Files\R\bin' is on PATH, or pass -RScriptPath."
}
$RScriptPath = $rscriptCommand.Source
$codeCommand = Get-Command 'code.cmd' -ErrorAction SilentlyContinue
if (-not $codeCommand) {
$fallbackCodePath = Join-Path $env:LOCALAPPDATA 'Programs\Microsoft VS Code\bin\code.cmd'
if (Test-Path -LiteralPath $fallbackCodePath -PathType Leaf) {
$codeCommand = Get-Item -LiteralPath $fallbackCodePath
}
else {
throw "The VS Code command-line launcher 'code' was not found. Add it to PATH or update this script with its location."
}
}
$documentsPath = [Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)
if ([string]::IsNullOrWhiteSpace($documentsPath)) {
$documentsPath = Join-Path $env:USERPROFILE 'Documents'
}
$scratchRoot = Join-Path $documentsPath 'R-scratch'
$projectPath = Join-Path $scratchRoot $projectName
if (Test-Path -LiteralPath $projectPath) {
throw "The destination '$projectPath' already exists. Choose a different project name."
}
if (-not (Test-Path -LiteralPath $scratchRoot -PathType Container)) {
New-Item -ItemType Directory -Path $scratchRoot | Out-Null
}
$rExpression = @'
project_path <- Sys.getenv(
"VSCODE_R_SCRATCH_PROJECT_PATH",
unset = NA_character_
)
if (is.na(project_path) || !nzchar(project_path)) {
stop("The scratch project path was not provided.", call. = FALSE)
}
if (!requireNamespace("usethis", quietly = TRUE)) {
stop(
"The R package 'usethis' is not installed. Run install.packages('usethis').",
call. = FALSE
)
}
usethis::create_project(
project_path,
rstudio = TRUE,
open = FALSE
)
'@
$localeVariableNames = @(
'LANG',
'LC_ALL',
'LC_COLLATE',
'LC_CTYPE',
'LC_MONETARY',
'LC_TIME'
)
$savedLocaleVariables = @{}
foreach ($localeVariableName in $localeVariableNames) {
$localeValue = [Environment]::GetEnvironmentVariable($localeVariableName, 'Process')
if ($null -ne $localeValue) {
$savedLocaleVariables[$localeVariableName] = $localeValue
}
[Environment]::SetEnvironmentVariable($localeVariableName, $null, 'Process')
}
$projectPathVariableName = 'VSCODE_R_SCRATCH_PROJECT_PATH'
$savedProjectPathVariable = [Environment]::GetEnvironmentVariable($projectPathVariableName, 'Process')
$temporaryRScriptPath = Join-Path (
[System.IO.Path]::GetTempPath()
) "vscode-r-scratch-$([guid]::NewGuid().ToString('N')).R"
try {
[System.IO.File]::WriteAllText(
$temporaryRScriptPath,
$rExpression,
[System.Text.UTF8Encoding]::new($false)
)
[Environment]::SetEnvironmentVariable($projectPathVariableName, $projectPath, 'Process')
& $RScriptPath --vanilla $temporaryRScriptPath
$rExitCode = $LASTEXITCODE
}
finally {
if (Test-Path -LiteralPath $temporaryRScriptPath -PathType Leaf) {
Remove-Item -LiteralPath $temporaryRScriptPath -Force
}
[Environment]::SetEnvironmentVariable(
$projectPathVariableName,
$savedProjectPathVariable,
'Process'
)
foreach ($localeVariableName in $localeVariableNames) {
$savedLocaleValue = $savedLocaleVariables[$localeVariableName]
[Environment]::SetEnvironmentVariable(
$localeVariableName,
$savedLocaleValue,
'Process'
)
}
}
if ($rExitCode -ne 0) {
throw "R could not create the project (exit code $rExitCode). Check '$projectPath' for any partially created files before trying again."
}
& $codeCommand.Source --new-window $projectPath
if ($LASTEXITCODE -ne 0) {
throw "The project was created at '$projectPath', but VS Code could not open it."
}
Write-Host "Created and opened R scratch project: $projectPath"Download new-r-scratch-project.ps1
To install the scripts on a new machine, put both .ps1 files in %USERPROFILE%\.vscode-scripts\.