diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6559ee..f325889 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
All notable changes to R Console will be documented in this file.
+## [0.2.7] - 2026-05-24
+
+### Added
+- Added the Command Palette command `R Console: Insert Pipe Operator`.
+- Added a configurable pipe-insertion keybinding, set to `Ctrl+Alt+M` by default in active R Console terminals.
+- Added `r.console.pipeOperator` to choose whether pipe insertion uses R's native pipe `|>` or the magrittr pipe `%>%`.
+
+### Changed
+- Improved console completions in data-aware contexts.
+- Field completions now quote column names such as `a b` correctly in member and bracket contexts.
+
## [0.2.6] - 2026-05-20
### Changed
diff --git a/README.md b/README.md
index ada8bca..a281316 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ Launch `R Console` from the Command Palette with:
- `R Console: Create R Console`
- `R Console: Create R Console in Side Editor`
- `R Console: Manage Persistent Sessions...`
+- `R Console: Insert Pipe Operator`
Use the session manager to attach to, detach from, or close running R Console sessions.
@@ -124,6 +125,7 @@ R Console also contributes its own settings:
| --- | --- | --- |
| `r.console.autoMatch` | `true` | Auto-insert matching brackets and quotes |
| `r.console.tabSize` | `2` | Indentation width |
+| `r.console.pipeOperator` | |> | Pipe operator inserted by `R Console: Insert Pipe Operator` / `Ctrl+Alt+M`; supported values are |> and `%>%` |
## Dependency Model
diff --git a/package-lock.json b/package-lock.json
index 9ca0c15..a3307bf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "vsc-r-console",
- "version": "0.2.6",
+ "version": "0.2.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vsc-r-console",
- "version": "0.2.6",
+ "version": "0.2.7",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@xterm/headless": "^6.0.0",
diff --git a/package.json b/package.json
index a7564ec..c4f0630 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "vsc-r-console",
"displayName": "R Console for VS Code",
"description": "A lightweight R console for VS Code",
- "version": "0.2.6",
+ "version": "0.2.7",
"publisher": "RConsole",
"license": "SEE LICENSE IN LICENSE",
"icon": "images/Rlogo.png",
@@ -74,8 +74,25 @@
"command": "r-console.managePersistentSessions",
"title": "Manage Persistent Sessions...",
"category": "R Console"
+ },
+ {
+ "command": "r-console.insertPipeOperator",
+ "title": "Insert Pipe Operator",
+ "category": "R Console"
+ }
+ ],
+ "keybindings": [
+ {
+ "command": "r-console.insertPipeOperator",
+ "key": "ctrl+alt+m",
+ "when": "terminalFocus && rConsole.consoleActive"
}
],
+ "configurationDefaults": {
+ "terminal.integrated.commandsToSkipShell": [
+ "r-console.insertPipeOperator"
+ ]
+ },
"configuration": {
"title": "R Console",
"properties": {
@@ -88,6 +105,15 @@
"type": "number",
"default": 2,
"description": "Number of spaces for indentation."
+ },
+ "r.console.pipeOperator": {
+ "type": "string",
+ "enum": [
+ "|>",
+ "%>%"
+ ],
+ "default": "|>",
+ "description": "Pipe operator to insert for chain operations. The default keybinding is Ctrl+Alt+M."
}
}
}
diff --git a/src/Language/completion.ts b/src/Language/completion.ts
index 7072745..5076349 100644
--- a/src/Language/completion.ts
+++ b/src/Language/completion.ts
@@ -7,6 +7,9 @@ type CompletionContext = {
triggerCharacter?: string;
objectName?: string;
operator?: "$" | "@";
+ bracketOperator?: "[" | "[[";
+ bracketQuote?: "\"" | "'";
+ chainedBracket?: boolean;
functionName?: string;
dataObjectName?: string;
snapshotInput: string;
@@ -22,6 +25,41 @@ type CompletionEntry = {
replaceStart?: number;
};
+const COMPLETION_GROUP_ORDER = {
+ argument: [
+ "Arguments",
+ "Fields",
+ "Runtime Variables",
+ "Runtime Functions",
+ "Packages",
+ "Functions",
+ "Recent Input",
+ "Other",
+ ],
+ bracket: ["Fields", "Runtime Variables", "Runtime Functions", "Recent Input", "Other"],
+ default: [
+ "Runtime Variables",
+ "Runtime Functions",
+ "Packages",
+ "Functions",
+ "Fields",
+ "Recent Input",
+ "Other",
+ ],
+ member: ["Fields", "Runtime Variables", "Runtime Functions", "Recent Input", "Other"],
+ package: ["Package Members", "Functions", "Fields", "Recent Input", "Other"],
+} as const satisfies Record;
+
+const DATA_CONTEXT_GROUP_ORDER = [
+ "Fields",
+ "Runtime Variables",
+ "Runtime Functions",
+ "Packages",
+ "Functions",
+ "Recent Input",
+ "Other",
+] as const;
+
export type CompletionPickItem = vscode.QuickPickItem & {
insertText: string;
replaceStart: number;
@@ -30,6 +68,8 @@ export type CompletionPickItem = vscode.QuickPickItem & {
source: "lsp" | "session" | "buffer";
};
+export type CompletionQuickPickItem = CompletionPickItem | vscode.QuickPickItem;
+
export interface CompletionProvider {
provideCompletionItems(
doc: vscode.TextDocument,
@@ -69,7 +109,13 @@ type WorkspaceData = {
};
const TOP_LEVEL_SYMBOL_PATTERN = /^[a-zA-Z._][a-zA-Z0-9._]*$/;
+const R_SYNTACTIC_NAME_PATTERN = /^(?:[a-zA-Z]|\.(?![0-9]))[a-zA-Z0-9._]*$/;
const MEMBER_CHAIN_SEGMENT = "(?:`[^`]+`|[a-zA-Z._][a-zA-Z0-9._]*)";
+const BRACKET_CHAIN_SEGMENT = "(?:\\[[^\\[\\]]*\\]|\\[\\[[^\\[\\]]*\\]\\])";
+const MEMBER_OBJECT_SEGMENT = `(?:${MEMBER_CHAIN_SEGMENT}(?:${BRACKET_CHAIN_SEGMENT})*)`;
+const BRACKET_OBJECT_PATTERN = new RegExp(
+ `([a-zA-Z._][a-zA-Z0-9._]*(?:${BRACKET_CHAIN_SEGMENT})*)(\\[\\[?)\\s*(["']?)([a-zA-Z0-9._]*)$`
+);
const CONSOLE_IDENTIFIER_PATTERN = /\b[a-zA-Z.][a-zA-Z0-9._]*\b/g;
const R_RESERVED_WORDS = new Set([
"if",
@@ -93,16 +139,44 @@ const R_RESERVED_WORDS = new Set([
"NA_character_",
]);
const MEMBER_CHAIN_TAIL_PATTERN = new RegExp(
- `(${MEMBER_CHAIN_SEGMENT}(?:\\s*[$@]\\s*${MEMBER_CHAIN_SEGMENT})*)\\s*$`
+ `(${MEMBER_OBJECT_SEGMENT}(?:\\s*[$@]\\s*${MEMBER_OBJECT_SEGMENT})*)\\s*$`
);
+function isPipePlaceholder(name: string): boolean {
+ return name === "_" || name === ".";
+}
+
+function quoteRNameIfNeeded(name: string): string {
+ if (R_SYNTACTIC_NAME_PATTERN.test(name) && !R_RESERVED_WORDS.has(name)) {
+ return name;
+ }
+ return `\`${name.replace(/\\/g, "\\\\").replace(/`/g, "\\`")}\``;
+}
+
+function getFieldInsertText(name: string, context: CompletionContext): string {
+ if (context.kind !== "bracket") {
+ return quoteRNameIfNeeded(name);
+ }
+ if (context.bracketQuote) {
+ const quotePattern = context.bracketQuote === "\"" ? /"/g : /'/g;
+ return name.replace(/\\/g, "\\\\").replace(quotePattern, `\\${context.bracketQuote}`);
+ }
+ if (context.bracketOperator === "[[") {
+ return `"${name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
+ }
+ return quoteRNameIfNeeded(name);
+}
+
function detectDataContext(beforeCursor: string): string | undefined {
- const pipePattern = /([a-zA-Z._][a-zA-Z0-9._]*)\s*(%>%|\|>)/g;
+ const pipePattern = new RegExp(
+ `([a-zA-Z._][a-zA-Z0-9._]*(?:${BRACKET_CHAIN_SEGMENT})*)\\s*(%>%|\\|>)`,
+ "g"
+ );
let pipeMatch;
let firstPipeObject: string | undefined;
while ((pipeMatch = pipePattern.exec(beforeCursor)) !== null) {
if (!firstPipeObject) {
- firstPipeObject = pipeMatch[1];
+ firstPipeObject = /^[a-zA-Z._][a-zA-Z0-9._]*/.exec(pipeMatch[1])?.[0];
}
}
if (firstPipeObject) {
@@ -115,9 +189,15 @@ function detectDataContext(beforeCursor: string): string | undefined {
if (/(%>%|\|>)\s*([a-zA-Z._][a-zA-Z0-9._]*::)?[a-zA-Z._][a-zA-Z0-9._]*\s*\([^)]*$/.test(afterPipe)) {
return firstPipeObject;
}
+ if (/(%>%)\s*\.\s*\[{1,2}[^\]]*$/.test(afterPipe)) {
+ return firstPipeObject;
+ }
if (/(\|>)\s*_\s*\[{1,2}[^\]]*$/.test(afterPipe)) {
return firstPipeObject;
}
+ if (/(\|>)\s*_\s*\[{1,2}[\s\S]*\]\$[a-zA-Z0-9._]*$/.test(afterPipe)) {
+ return firstPipeObject;
+ }
}
}
@@ -125,7 +205,7 @@ function detectDataContext(beforeCursor: string): string | undefined {
const bracketMatch = bracketPattern.exec(beforeCursor);
if (bracketMatch) {
const objectName = bracketMatch[1];
- if (objectName === '_' && firstPipeObject) {
+ if (isPipePlaceholder(objectName) && firstPipeObject) {
return firstPipeObject;
}
return objectName;
@@ -164,21 +244,23 @@ export function getCompletionContext(
const textForDataContext = fullTextBeforeCursor ?? beforeCursor;
const dataObjectName = detectDataContext(textForDataContext);
- const bracketMatch = /([a-zA-Z._][a-zA-Z0-9._]*)\[\[?\s*(["']?)([a-zA-Z0-9._]*)$/.exec(
- beforeCursor
- );
+ const bracketMatch = BRACKET_OBJECT_PATTERN.exec(beforeCursor);
if (bracketMatch) {
- const prefix = bracketMatch[3] || "";
+ const bracketOperator = bracketMatch[2] as "[" | "[[";
+ const prefix = bracketMatch[4] || "";
const bracketObject = bracketMatch[1];
- const effectiveDataObject = (bracketObject === '_' && dataObjectName)
- ? dataObjectName
- : bracketObject;
+ const baseObject = /^[a-zA-Z._][a-zA-Z0-9._]*/.exec(bracketObject)?.[0] ?? bracketObject;
+ const isPlaceholderBracket = isPipePlaceholder(baseObject) && !!dataObjectName;
+ const effectiveDataObject = isPlaceholderBracket ? dataObjectName : baseObject;
return {
kind: "bracket",
prefix,
replaceStart: beforeCursor.length - prefix.length,
- triggerCharacter: bracketMatch[2] ? undefined : "[",
- objectName: bracketObject,
+ triggerCharacter: bracketMatch[3] ? undefined : "[",
+ objectName: baseObject,
+ bracketOperator,
+ bracketQuote: bracketMatch[3] ? bracketMatch[3] as "\"" | "'" : undefined,
+ chainedBracket: bracketObject !== baseObject || isPlaceholderBracket,
dataObjectName: effectiveDataObject,
operator: undefined,
snapshotInput,
@@ -194,13 +276,20 @@ export function getCompletionContext(
const chainMatch = MEMBER_CHAIN_TAIL_PATTERN.exec(objectExprRaw);
const objectExpr = chainMatch?.[1];
if (objectExpr) {
+ const isBracketChainMember = operator === "$" && objectExpr.includes("[");
+ const baseObject = isBracketChainMember
+ ? /^[a-zA-Z._][a-zA-Z0-9._]*/.exec(objectExpr)?.[0]
+ : undefined;
+ const isPlaceholderMember = !!baseObject && isPipePlaceholder(baseObject) && !!dataObjectName;
return {
kind: "member",
prefix,
replaceStart: beforeCursor.length - prefix.length,
triggerCharacter: prefix.length === 0 ? operator : undefined,
- objectName: objectExpr,
+ objectName: isPlaceholderMember ? dataObjectName : baseObject ?? objectExpr,
operator,
+ chainedBracket: isBracketChainMember,
+ dataObjectName: isPlaceholderMember ? dataObjectName : undefined,
snapshotInput,
snapshotCursor,
};
@@ -279,25 +368,47 @@ export async function collectCompletionEntries(
context.kind === "member"
? await getRuntimeMemberCompletions(context, completionProvider)
: [];
- const lspItems =
- context.kind === "bracket"
+ const includeLspItems =
+ context.kind !== "bracket" ||
+ (context.bracketOperator === "[" && !context.bracketQuote && context.prefix.length > 0);
+ const rawLspItems =
+ !includeLspItems
? []
: await getLanguageServerCompletions(context, doc, position, multilineBuffer, completionProvider);
+ const lspItems =
+ context.kind === "default"
+ ? filterShadowedWorkspaceEntries(rawLspItems, sessionItems)
+ : rawLspItems;
const bufferItems = getConsoleBufferCompletions(
context,
doc.getText(),
recentConsoleEntries
);
- const columnItems = getDataColumnCompletions(context, sessionData);
+ const cachedColumnItems = getDataColumnCompletions(context, sessionData);
+ const columnItems =
+ cachedColumnItems.length > 0
+ ? cachedColumnItems
+ : await getRuntimeDataColumnCompletions(context, completionProvider);
const fallbackBufferItems = filterShadowedBufferEntries(bufferItems, [
...lspItems,
...sessionItems,
+ ...runtimeMemberItems,
...columnItems,
]);
if (context.kind === "bracket") {
const columnFiltered = filterCompletionEntries(columnItems, context.prefix);
+ const lspFiltered = filterCompletionEntries(lspItems, context.prefix);
const bufferFiltered = filterCompletionEntries(fallbackBufferItems, context.prefix);
+ const exactColumnMatch = columnFiltered.some(
+ (entry) => entry.label.toLowerCase() === context.prefix.toLowerCase()
+ );
+ if (lspFiltered.length > 0 && !exactColumnMatch) {
+ return dedupeCompletionEntries([...lspFiltered, ...columnFiltered, ...bufferFiltered]);
+ }
+ if (context.chainedBracket && bufferFiltered.length > 0) {
+ return dedupeCompletionEntries([...columnFiltered, ...bufferFiltered]);
+ }
if (columnFiltered.length > 0) {
return dedupeCompletionEntries(columnFiltered);
}
@@ -314,21 +425,7 @@ export async function collectCompletionEntries(
const bufferFiltered = filterCompletionEntries(fallbackBufferItems, context.prefix);
if (context.dataObjectName && columnFiltered.length > 0) {
- if (context.prefix.length === 0) {
- return dedupeCompletionEntries([
- ...columnFiltered,
- ...lspFiltered,
- ...sessionFiltered,
- ...bufferFiltered,
- ]);
- } else {
- return dedupeCompletionEntries([
- ...columnFiltered,
- ...lspFiltered,
- ...sessionFiltered,
- ...bufferFiltered,
- ]);
- }
+ return dedupeCompletionEntries(columnFiltered);
}
if (context.prefix.length === 0) {
@@ -349,19 +446,20 @@ export async function collectCompletionEntries(
if (context.kind === "member") {
const runtimeFiltered = filterCompletionEntries(runtimeMemberItems, context.prefix);
const sessionFiltered = filterCompletionEntries(sessionItems, context.prefix);
+ const bufferFiltered = filterCompletionEntries(fallbackBufferItems, context.prefix);
+ if (context.chainedBracket && bufferFiltered.length > 0) {
+ return dedupeCompletionEntries([...runtimeFiltered, ...sessionFiltered, ...bufferFiltered]);
+ }
if (runtimeFiltered.length > 0) {
return dedupeCompletionEntries(runtimeFiltered);
}
- return dedupeCompletionEntries(sessionFiltered);
+ return dedupeCompletionEntries([...sessionFiltered, ...bufferFiltered]);
}
const defaultColumnFiltered = filterCompletionEntries(columnItems, context.prefix);
if (context.dataObjectName && defaultColumnFiltered.length > 0) {
- return dedupeCompletionEntries(filterCompletionEntries(
- [...columnItems, ...lspItems, ...sessionItems, ...fallbackBufferItems],
- context.prefix
- ));
+ return dedupeCompletionEntries(defaultColumnFiltered);
}
return dedupeCompletionEntries(filterCompletionEntries(
@@ -388,6 +486,96 @@ export function toCompletionPick(
};
}
+export function toCompletionQuickPickItems(
+ entries: CompletionEntry[],
+ context: CompletionContext
+): CompletionQuickPickItem[] {
+ const grouped = new Map();
+
+ for (const entry of entries) {
+ const group = getCompletionGroup(entry, context);
+ const groupEntries = grouped.get(group) ?? [];
+ groupEntries.push(entry);
+ grouped.set(group, groupEntries);
+ }
+
+ const result: CompletionQuickPickItem[] = [];
+ const preferredGroups =
+ context.kind === "default" && context.dataObjectName
+ ? DATA_CONTEXT_GROUP_ORDER
+ : COMPLETION_GROUP_ORDER[context.kind];
+ const orderedGroups = new Set(preferredGroups);
+ const groups = [
+ ...preferredGroups,
+ ...[...grouped.keys()].filter((group) => !orderedGroups.has(group)),
+ ];
+
+ for (const group of groups) {
+ const groupEntries = grouped.get(group);
+ if (!groupEntries || groupEntries.length === 0) {
+ continue;
+ }
+ result.push({
+ label: group,
+ kind: vscode.QuickPickItemKind.Separator,
+ });
+ result.push(...groupEntries.map((entry) => toCompletionPick(entry, context)));
+ }
+
+ return result;
+}
+
+export function isCompletionPickItem(
+ item: CompletionQuickPickItem | undefined
+): item is CompletionPickItem {
+ return (
+ !!item &&
+ "insertText" in item &&
+ "replaceStart" in item &&
+ "snapshotInput" in item &&
+ "snapshotCursor" in item
+ );
+}
+
+function getCompletionGroup(
+ entry: CompletionEntry,
+ context: CompletionContext
+): string {
+ const kind = entry.kind;
+
+ if (entry.source === "buffer") {
+ return "Recent Input";
+ }
+ if (context.kind === "package") {
+ return "Package Members";
+ }
+ if (context.kind === "argument" && entry.source === "lsp") {
+ return "Arguments";
+ }
+ if (kind === vscode.CompletionItemKind.Field || kind === vscode.CompletionItemKind.Property) {
+ return "Fields";
+ }
+ if (entry.source === "session") {
+ return isCallableCompletionKind(kind) ? "Runtime Functions" : "Runtime Variables";
+ }
+ if (kind === vscode.CompletionItemKind.Module) {
+ return "Packages";
+ }
+ if (isCallableCompletionKind(kind)) {
+ return "Functions";
+ }
+
+ return "Other";
+}
+
+function isCallableCompletionKind(kind: vscode.CompletionItemKind | undefined): boolean {
+ return (
+ kind === vscode.CompletionItemKind.Function ||
+ kind === vscode.CompletionItemKind.Method ||
+ kind === vscode.CompletionItemKind.Constructor
+ );
+}
+
function getSessionCompletions(
context: CompletionContext,
data: WorkspaceData | undefined
@@ -412,7 +600,7 @@ function getSessionCompletions(
: obj.names || [];
return members.map((name) => ({
label: name,
- insertText: name,
+ insertText: getFieldInsertText(name, context),
kind: vscode.CompletionItemKind.Field,
detail: context.objectName,
source: "session",
@@ -455,30 +643,64 @@ function getDataColumnCompletions(
return obj.names.map((name) => ({
label: name,
- insertText: name,
+ insertText: getFieldInsertText(name, context),
kind: vscode.CompletionItemKind.Field,
detail: context.dataObjectName,
source: "session" as const,
}));
}
+async function getRuntimeDataColumnCompletions(
+ context: CompletionContext,
+ completionProvider?: CompletionProvider
+): Promise {
+ if (!context.dataObjectName || !completionProvider?.provideMemberCompletionItems) {
+ return [];
+ }
+
+ try {
+ const items = await completionProvider.provideMemberCompletionItems(
+ context.dataObjectName,
+ "$"
+ );
+ if (!items || items.length === 0) {
+ return [];
+ }
+ return items
+ .filter((item) => typeof item.name === "string" && item.name.length > 0)
+ .map((item) => ({
+ label: item.name,
+ insertText: getFieldInsertText(item.name, context),
+ kind: vscode.CompletionItemKind.Field,
+ detail: context.dataObjectName,
+ source: "session" as const,
+ }));
+ } catch {
+ return [];
+ }
+}
+
function getConsoleBufferCompletions(
context: CompletionContext,
currentInputText: string,
recentConsoleEntries: string[]
): CompletionEntry[] {
if (
- context.prefix.length === 0 ||
+ (context.prefix.length === 0 &&
+ !((context.kind === "bracket" || context.kind === "member") && context.chainedBracket)) ||
(context.kind !== "default" &&
context.kind !== "argument" &&
- context.kind !== "bracket")
+ context.kind !== "bracket" &&
+ !(context.kind === "member" && context.chainedBracket))
) {
return [];
}
const result: CompletionEntry[] = [];
const seen = new Set();
- const sources = [currentInputText, ...[...recentConsoleEntries].reverse()];
+ const sources = context.prefix.length === 0
+ ? [currentInputText]
+ : [currentInputText, ...[...recentConsoleEntries].reverse()];
for (const sourceText of sources) {
const matches = sourceText.match(CONSOLE_IDENTIFIER_PATTERN);
@@ -489,6 +711,8 @@ function getConsoleBufferCompletions(
for (const label of matches) {
if (
label === context.prefix ||
+ label === context.objectName ||
+ label === context.dataObjectName ||
R_RESERVED_WORDS.has(label) ||
seen.has(label)
) {
@@ -542,7 +766,7 @@ async function getRuntimeMemberCompletions(
/^\s*function\s*\(/.test(item.str || "");
return {
label: item.name,
- insertText: item.name,
+ insertText: quoteRNameIfNeeded(item.name),
kind: isFunction
? vscode.CompletionItemKind.Function
: vscode.CompletionItemKind.Field,
@@ -685,6 +909,23 @@ function filterShadowedBufferEntries(
);
}
+function filterShadowedWorkspaceEntries(
+ entries: CompletionEntry[],
+ workspaceEntries: CompletionEntry[]
+): CompletionEntry[] {
+ if (entries.length === 0 || workspaceEntries.length === 0) {
+ return entries;
+ }
+
+ const workspaceLabels = new Set(
+ workspaceEntries.map((entry) => entry.label.toLowerCase())
+ );
+
+ return entries.filter(
+ (entry) => !workspaceLabels.has(entry.label.toLowerCase())
+ );
+}
+
function dedupeCompletionEntries(entries: CompletionEntry[]): CompletionEntry[] {
const seen = new Set();
const result: CompletionEntry[] = [];
diff --git a/src/Terminal/rTerminal.ts b/src/Terminal/rTerminal.ts
index 78aeb40..c9c28af 100644
--- a/src/Terminal/rTerminal.ts
+++ b/src/Terminal/rTerminal.ts
@@ -213,6 +213,7 @@ export class RTerminal implements vscode.Pseudoterminal {
private autoMatch = true;
private tabSize = 2;
+ private pipeOperator: "|>" | "%>%" = "|>";
private lastSearchTerm = "";
constructor(
@@ -354,6 +355,8 @@ export class RTerminal implements vscode.Pseudoterminal {
const config = vscode.workspace.getConfiguration("r.console");
this.autoMatch = config.get("autoMatch", true);
this.tabSize = config.get("tabSize", 2);
+ this.pipeOperator =
+ config.get("pipeOperator", "|>") === "%>%" ? "%>%" : "|>";
}
private runtimeHost(): RuntimeHost {
@@ -594,6 +597,18 @@ export class RTerminal implements vscode.Pseudoterminal {
}
}
+ public insertPipeOperator(): void {
+ if (this.mode !== "ready" || !this.promptReady) {
+ return;
+ }
+
+ this.ensureReadyPromptVisibleForInput();
+ this.escPendingClear = false;
+ this.expandForEdit();
+ this.inputState.insertText(` ${this.pipeOperator} `);
+ this.renderInput();
+ }
+
private resolveRuntimeBackend(): RuntimeBackend | undefined {
return createRuntimeBackend(this.extensionPath);
}
diff --git a/src/Terminal/rTerminal/lang.ts b/src/Terminal/rTerminal/lang.ts
index e4b1155..544e790 100644
--- a/src/Terminal/rTerminal/lang.ts
+++ b/src/Terminal/rTerminal/lang.ts
@@ -3,7 +3,8 @@ import {
CompletionPickItem,
collectCompletionEntries,
getCompletionContext,
- toCompletionPick,
+ isCompletionPickItem,
+ toCompletionQuickPickItems,
} from "../../Language/completion";
import {
ConsoleLspClient,
@@ -115,14 +116,14 @@ export class RTermLang {
return;
}
- const picks = entries.map((entry) => toCompletionPick(entry, context));
+ const picks = toCompletionQuickPickItems(entries, context);
const selection = await vscode.window.showQuickPick(picks, {
matchOnDescription: true,
matchOnDetail: true,
placeHolder: "R console completions",
});
- if (!selection) {
+ if (!isCompletionPickItem(selection)) {
return;
}
diff --git a/src/extension.ts b/src/extension.ts
index a492b2d..04beda5 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -59,6 +59,7 @@ const ignoredEditorClosePids: Set = new Set();
const closeConfirmationInProgress = new WeakSet();
const ignoredTerminalCloseEvents = new WeakSet();
const R_CONSOLE_PID_LABEL_PATTERN = /^R Console \((\d+)\)$/;
+const R_CONSOLE_ACTIVE_CONTEXT = "rConsole.consoleActive";
const PERSIST_DEBOUNCE_MS = 250;
const PERSIST_HEARTBEAT_MS = 5000;
let extensionBaseUri: vscode.Uri | undefined;
@@ -104,6 +105,9 @@ export async function activate(context: vscode.ExtensionContext): Promise
vscode.commands.registerCommand("r-console.managePersistentSessions", () => {
void managePersistentSessions(context);
}),
+ vscode.commands.registerCommand("r-console.insertPipeOperator", () => {
+ insertPipeOperatorInActiveConsole();
+ }),
vscode.window.onDidOpenTerminal(handleTerminalOpen),
vscode.window.onDidChangeActiveTerminal(handleActiveTerminalChange),
vscode.window.onDidCloseTerminal(handleTerminalClose),
@@ -124,6 +128,7 @@ export async function activate(context: vscode.ExtensionContext): Promise
disposeStalePersistentTerminalViews();
syncTerminalRecordsFromWindow();
+ syncRConsoleActiveContext();
void ensureConfiguredRPath();
}
@@ -397,13 +402,18 @@ async function handleTerminalClose(closedTerminal: vscode.Terminal): Promise {
let sessions = await refreshManagedPersistentSessions();
if (sessions.length === 0) {
@@ -1018,18 +1046,20 @@ function attachTerminal(
const preserveFocus =
preserveFocusOverride ?? alwaysUseActive === false;
terminal.show(preserveFocus);
+ syncRConsoleActiveContext();
return terminal;
}
function handleTerminalOpen(terminal: vscode.Terminal): void {
syncTerminalRecord(terminal);
+ syncRConsoleActiveContext();
}
function handleActiveTerminalChange(terminal: vscode.Terminal | undefined): void {
- if (!terminal) {
- return;
+ if (terminal) {
+ syncTerminalRecord(terminal);
}
- syncTerminalRecord(terminal);
+ syncRConsoleActiveContext();
}
function resolveRecordFromTerminal(