Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/configuration.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ skip = []
## Configuration recommendations

For collaborative projects, we recommend creating an `air.toml` and placing it at your project root even if you plan to use the default Air settings.
The easiest way to do this is by running [`usethis::use_air()`](https://usethis.r-lib.org/dev/reference/use_air.html) (note that this currently requires the development version of usethis).
The easiest way to do this is by running the VS Code or Positron command `Air: Initialize Workspace Folder`, or by running [`usethis::use_air()`](https://usethis.r-lib.org/dev/reference/use_air.html) if you are using another IDE.

The existence of an `air.toml` has a number of benefits:

Expand Down
20 changes: 14 additions & 6 deletions docs/editor-vscode.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ Air provides first class support for both VS Code and Positron, which both suppo

# Installation

Air is available [as an Extension](https://marketplace.visualstudio.com/items?itemName=Posit.air-vscode) for both VS Code and Positron.
The extension comes pre-bundled with an Air binary, so you don't need anything else to get going!
The Air extension is hosted in the VS Code Marketplace and on OpenVSX.
Air is available as a [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=Posit.air-vscode) for VS Code.

Air comes pre-bundled with Positron.
It is available as an [OpenVSX Extension](https://open-vsx.org/extension/posit/air-vscode) that is already installed for you.

The extension for both VS Code and Positron comes with an Air binary, so you don't need anything else to get going!

## User vs Workspace settings

Expand All @@ -27,7 +30,8 @@ We generally recommend modifying workspace level settings for two reasons:
- User level settings are automatically applied for *all* projects that you open.
While this sounds nice, if you open an older project (or a project you don't own) that doesn't use Air, then you'll have to remember to turn off your user level Air settings before committing to that project, otherwise you may create a large amount of format related diffs that the project may not want.

The easiest way to set up a workspace level `settings.json` with the recommended settings is by running [`usethis::use_air()`](https://usethis.r-lib.org/dev/reference/use_air.html) (note that this currently requires the development version of usethis).
The easiest way to set up a workspace level `settings.json` with the recommended settings is by running the command `Air: Initialize Workspace Folder`.
This is equivalent to running [`usethis::use_air()`](https://usethis.r-lib.org/dev/reference/use_air.html) from an R console.

Alternatively, to open your `settings.json` file from the Command Palette (`Cmd + P` on Mac/Linux, `Ctrl + P` on Windows):

Expand All @@ -49,8 +53,10 @@ Once you have the extension installed, turn on Format on Save for R documents by
}
```

Note that running the command `Air: Initialize Workspace Folder` will add this to your `settings.json` for you.

You should now be able to simply open an R document, save it, and have the entire document formatted by Air.
You can also explicitly call the command `Format Document` if you'd like to control this manually.
You can also explicitly call the command `Format Document` if you'd like to control the timing of formatting manually.

<!--# Come back and add video -->

Expand All @@ -67,7 +73,7 @@ Air ships with a special `Air: Format Workspace Folder` command to format all R
This is particularly useful when transitioning an existing project over to Air, where you need to perform a project-wide format before utilizing the per-file format on save feature.

Note that if you don't have an `air.toml` in your project, then this command will use Air's default settings rather than the IDE [settings synchronization mechanism](configuration.qmd#configuration-settings-synchronization).
We recommend using `usethis::use_air()` to set up an `air.toml` (among other things) before running this command.
We recommend using the command `Air: Initialize Workspace Folder` to set up an `air.toml` (among other things) before running this command.

## Quarto

Expand All @@ -90,6 +96,8 @@ To format all R code cells on save, set this in your `settings.json`:
}
```

Note that running the command `Air: Initialize Workspace Folder` will add this to your `settings.json` for you.

<!--# Come back and add video -->

### Format cell
Expand Down
2 changes: 2 additions & 0 deletions editors/code/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

## Development version

- New `Air: Initialize Workspace Folder` command to initialize a project for use with Air. This supercedes `usethis::use_air()` for VS Code and Positron users by providing a holistic setup experience from within the IDE (#323).


## 0.14.0

Expand Down
5 changes: 5 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@
"category": "Air",
"command": "air.restart"
},
{
"title": "Initialize Workspace Folder",
"category": "Air",
"command": "air.workspaceFolderInitialization"
},
{
"title": "Format Workspace Folder",
"category": "Air",
Expand Down
227 changes: 54 additions & 173 deletions editors/code/src/command/workspace-folder-formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import * as vscode from "vscode";
import { Cmd, Ctx } from "../context";
import * as output from "../output";
import { isError, isResult, runCommand } from "../process";
import {
saveAllDirtyWorkspaceTextDocuments,
selectWorkspaceFolder,
} from "../workspace";

/**
* Format a workspace folder
*
* # Workspace folder selection
*
* - If 0 workspace folders are open, errors
* - If 1 workspace folder is open, automatically uses it
* - If >1 workspace folders are open, asks the user to choose
*
* # Tab saving
*
* Because we use the embedded Air CLI to perform the formatting, we force all
Expand Down Expand Up @@ -42,195 +40,78 @@ import { isError, isResult, runCommand } from "../process";
* `{root}/R/air.toml` that would handle any R files in your project. Instead,
* we hope this isn't common enough to come up much in practice.
*/
export function workspaceFolderFormatting(ctx: Ctx): Cmd {
return async () => {
Comment on lines -45 to -46
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no actual changes to this function, I've just de-dented it one level since it no longer returns a callback

const binaryPath = ctx.lsp.getBinaryPath();

const workspaceFolder = await selectWorkspaceFolder();
if (!workspaceFolder) {
return;
}
export async function workspaceFolderFormatting(
workspaceFolder: vscode.WorkspaceFolder,
binaryPath: string,
) {
const allSaved = await saveAllDirtyWorkspaceTextDocuments(workspaceFolder);
if (!allSaved) {
return;
}

const allSaved =
await saveAllDirtyWorkspaceTextDocuments(workspaceFolder);
if (!allSaved) {
return;
}
const workspaceFolderPath = workspaceFolder.uri.fsPath;

const workspaceFolderPath = workspaceFolder.uri.fsPath;
// i.e., `air format {workspaceFolderPath} --no-color`
const args = ["format", workspaceFolderPath, "--no-color"];

// i.e., `air format {workspaceFolderPath} --no-color`
const args = ["format", workspaceFolderPath, "--no-color"];
// This should not matter since the path is explicitly supplied, but better to be safe
const options = {
cwd: workspaceFolderPath,
};

// This should not matter since the path is explicitly supplied, but better to be safe
const options = {
cwd: workspaceFolderPath,
};
// Resolves when the spawned process closes or errors
const result = await runCommand(binaryPath, args, options);

// Resolves when the spawned process closes or errors
const result = await runCommand(binaryPath, args, options);
let anyErrors = false;

let anyErrors = false;
if (isError(result)) {
// Something went horribly wrong in the process spawning or shutdown process
anyErrors = true;
output.log(
`Errors occurred while formatting the ${workspaceFolder.name} workspace folder.\n${result.error.message}`,
);
}

if (isError(result)) {
// Something went horribly wrong in the process spawning or shutdown process
anyErrors = true;
if (isResult(result)) {
if (result.code !== 0) {
// Air was able to run and exit, but we had an error along the way
output.log(
`Errors occurred while formatting the ${workspaceFolder.name} workspace folder.\n${result.error.message}`,
`Errors occurred while formatting the ${workspaceFolder.name} workspace folder.\n${result.stderr}`,
);
anyErrors = true;
}

if (isResult(result)) {
if (result.code !== 0) {
// Air was able to run and exit, but we had an error along the way
output.log(
`Errors occurred while formatting the ${workspaceFolder.name} workspace folder.\n${result.stderr}`,
);
anyErrors = true;
}
}

if (anyErrors) {
const answer = await vscode.window.showInformationMessage(
`Errors occurred while formatting the ${workspaceFolder.name} workspace folder. View the logs?`,
{ modal: true },
"Yes",
"No",
);

if (answer === "Yes") {
output.show();
}

return;
}

vscode.window.showInformationMessage(
`Successfully formatted the ${workspaceFolder.name} workspace folder.`,
);
};
}

async function selectWorkspaceFolder(): Promise<
vscode.WorkspaceFolder | undefined
> {
const workspaceFolders = vscode.workspace.workspaceFolders;

if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showErrorMessage(
"You must be inside a workspace to format a workspace folder.",
);
return undefined;
}

if (workspaceFolders.length === 1) {
return workspaceFolders[0];
}

// Let the user select a workspace folder if >1 are open, may be
// `undefined` if user bails from quick pick!
const workspaceFolder =
await selectWorkspaceFolderFromQuickPick(workspaceFolders);

return workspaceFolder;
}

async function selectWorkspaceFolderFromQuickPick(
workspaceFolders: readonly vscode.WorkspaceFolder[],
): Promise<vscode.WorkspaceFolder | undefined> {
// Show the workspace names
const workspaceFolderNames = workspaceFolders.map(
(workspaceFolder) => workspaceFolder.name,
);

const workspaceFolderName = await vscode.window.showQuickPick(
workspaceFolderNames,
{
canPickMany: false,
title: "Which workspace folder should be formatted?",
},
);

if (!workspaceFolderName) {
// User bailed from the quick pick
return undefined;
}
if (anyErrors) {
const answer = await vscode.window.showInformationMessage(
`Errors occurred while formatting the ${workspaceFolder.name} workspace folder. View the logs?`,
{ modal: true },
"Yes",
"No",
);

// Match selected name back to the workspace folder
for (let workspaceFolder of workspaceFolders) {
if (workspaceFolder.name === workspaceFolderName) {
return workspaceFolder;
if (answer === "Yes") {
output.show();
}
}

// Should never get here
output.log(
`Matched a workspace folder name, but unexpectedly can't find corresponding workspace folder. Folder name: ${workspaceFolderName}.`,
);
return undefined;
}

/**
* Save all open dirty editor tabs relevant to the workspace folder
*
* - Filters to only tabs living under the chosen workspace folder
* - Asks the user if they are okay with us saving the editor tabs
*/
async function saveAllDirtyWorkspaceTextDocuments(
workspaceFolder: vscode.WorkspaceFolder,
): Promise<boolean> {
const textDocuments = dirtyWorkspaceTextDocuments(workspaceFolder);

if (textDocuments.length === 0) {
// Nothing to save!
return true;
return;
}

// Ask the user if we can save them
const answer = await vscode.window.showInformationMessage(
`All editors within the ${workspaceFolder.name} workspace folder must be saved before formatting. Proceed with saving these editors?`,
{ modal: true },
"Yes",
"No",
vscode.window.showInformationMessage(
`Successfully formatted the ${workspaceFolder.name} workspace folder.`,
);

if (answer !== "Yes") {
// User said `"No"` or bailed from the menu
return false;
}

// Save all documents, and ensure that all successfully saved
const savedPromises = textDocuments.map((textDocument) =>
textDocument.save(),
);
const saved = await Promise.all(savedPromises);
return saved.every((save) => save);
}

function dirtyWorkspaceTextDocuments(
workspaceFolder: vscode.WorkspaceFolder,
): vscode.TextDocument[] {
return vscode.workspace.textDocuments.filter((document) => {
if (document.isClosed) {
// Not actually synchonized. This document will be refreshed when the document is reopened.
return false;
}

if (!document.isDirty) {
// Nothing to do
return false;
}
export function workspaceFolderFormattingCallback(ctx: Ctx): Cmd {
return async () => {
const workspaceFolder = await selectWorkspaceFolder();

if (document.isUntitled) {
// These aren't part of the workspace folder
return false;
if (!workspaceFolder) {
return;
}

if (!document.uri.fsPath.startsWith(workspaceFolder.uri.fsPath)) {
// The document must live "under" the chosen workspace folder for us to care about it
return false;
}
const binaryPath = ctx.lsp.getBinaryPath();

return true;
});
await workspaceFolderFormatting(workspaceFolder, binaryPath);
};
}
Loading
Loading