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(