From 25de24e5e93f3d42671590927e675578fb5ea6df Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Sat, 23 May 2026 12:54:58 +1000 Subject: [PATCH 1/6] feat(console): group R completions and prefer runtime workspace data - Add Quick Pick separators for console completion groups, keep workspace objects - authoritative over duplicate LSP completions, and resolve data-column completions from a single runtime source. - Also support piped data bracket contexts such as `%>% .[]` and `|> _[]`. --- src/Language/completion.ts | 218 +++++++++++++++++++++++++++++---- src/Terminal/rTerminal/lang.ts | 7 +- 2 files changed, 199 insertions(+), 26 deletions(-) diff --git a/src/Language/completion.ts b/src/Language/completion.ts index 7072745..c2cecff 100644 --- a/src/Language/completion.ts +++ b/src/Language/completion.ts @@ -22,6 +22,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 +65,8 @@ export type CompletionPickItem = vscode.QuickPickItem & { source: "lsp" | "session" | "buffer"; }; +export type CompletionQuickPickItem = CompletionPickItem | vscode.QuickPickItem; + export interface CompletionProvider { provideCompletionItems( doc: vscode.TextDocument, @@ -96,6 +133,10 @@ const MEMBER_CHAIN_TAIL_PATTERN = new RegExp( `(${MEMBER_CHAIN_SEGMENT}(?:\\s*[$@]\\s*${MEMBER_CHAIN_SEGMENT})*)\\s*$` ); +function isPipePlaceholder(name: string): boolean { + return name === "_" || name === "."; +} + function detectDataContext(beforeCursor: string): string | undefined { const pipePattern = /([a-zA-Z._][a-zA-Z0-9._]*)\s*(%>%|\|>)/g; let pipeMatch; @@ -115,6 +156,9 @@ 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; } @@ -125,7 +169,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; @@ -170,7 +214,7 @@ export function getCompletionContext( if (bracketMatch) { const prefix = bracketMatch[3] || ""; const bracketObject = bracketMatch[1]; - const effectiveDataObject = (bracketObject === '_' && dataObjectName) + const effectiveDataObject = (isPipePlaceholder(bracketObject) && dataObjectName) ? dataObjectName : bracketObject; return { @@ -279,16 +323,24 @@ export async function collectCompletionEntries( context.kind === "member" ? await getRuntimeMemberCompletions(context, completionProvider) : []; - const lspItems = + const rawLspItems = context.kind === "bracket" ? [] : 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, @@ -314,21 +366,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) { @@ -358,10 +396,7 @@ export async function collectCompletionEntries( 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 +423,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 @@ -462,6 +587,36 @@ function getDataColumnCompletions( })); } +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: item.name, + kind: vscode.CompletionItemKind.Field, + detail: context.dataObjectName, + source: "session" as const, + })); + } catch { + return []; + } +} + function getConsoleBufferCompletions( context: CompletionContext, currentInputText: string, @@ -685,6 +840,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/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; } From 14ca6fc6662b80523659aa497ca0994963878910 Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Sat, 23 May 2026 13:28:52 +1000 Subject: [PATCH 2/6] Implemented non-syntactic field-name insertion --- src/Language/completion.ts | 41 +++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/Language/completion.ts b/src/Language/completion.ts index c2cecff..c07a14c 100644 --- a/src/Language/completion.ts +++ b/src/Language/completion.ts @@ -7,6 +7,8 @@ type CompletionContext = { triggerCharacter?: string; objectName?: string; operator?: "$" | "@"; + bracketOperator?: "[" | "[["; + bracketQuote?: "\"" | "'"; functionName?: string; dataObjectName?: string; snapshotInput: string; @@ -106,6 +108,7 @@ 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 CONSOLE_IDENTIFIER_PATTERN = /\b[a-zA-Z.][a-zA-Z0-9._]*\b/g; const R_RESERVED_WORDS = new Set([ @@ -137,6 +140,27 @@ 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; let pipeMatch; @@ -208,11 +232,12 @@ 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( + const bracketMatch = /([a-zA-Z._][a-zA-Z0-9._]*)(\[\[?)\s*(["']?)([a-zA-Z0-9._]*)$/.exec( beforeCursor ); if (bracketMatch) { - const prefix = bracketMatch[3] || ""; + const bracketOperator = bracketMatch[2] as "[" | "[["; + const prefix = bracketMatch[4] || ""; const bracketObject = bracketMatch[1]; const effectiveDataObject = (isPipePlaceholder(bracketObject) && dataObjectName) ? dataObjectName @@ -221,8 +246,10 @@ export function getCompletionContext( kind: "bracket", prefix, replaceStart: beforeCursor.length - prefix.length, - triggerCharacter: bracketMatch[2] ? undefined : "[", + triggerCharacter: bracketMatch[3] ? undefined : "[", objectName: bracketObject, + bracketOperator, + bracketQuote: bracketMatch[3] ? bracketMatch[3] as "\"" | "'" : undefined, dataObjectName: effectiveDataObject, operator: undefined, snapshotInput, @@ -537,7 +564,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", @@ -580,7 +607,7 @@ 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, @@ -607,7 +634,7 @@ async function getRuntimeDataColumnCompletions( .filter((item) => typeof item.name === "string" && item.name.length > 0) .map((item) => ({ label: item.name, - insertText: item.name, + insertText: getFieldInsertText(item.name, context), kind: vscode.CompletionItemKind.Field, detail: context.dataObjectName, source: "session" as const, @@ -697,7 +724,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, From 24e07b80ffa946b12b1557b3710a5541b468ad5f Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Sat, 23 May 2026 13:38:15 +1000 Subject: [PATCH 3/6] fix(console): allow function completions inside data brackets --- src/Language/completion.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Language/completion.ts b/src/Language/completion.ts index c07a14c..f573610 100644 --- a/src/Language/completion.ts +++ b/src/Language/completion.ts @@ -350,8 +350,11 @@ export async function collectCompletionEntries( context.kind === "member" ? await getRuntimeMemberCompletions(context, completionProvider) : []; + const includeLspItems = + context.kind !== "bracket" || + (context.bracketOperator === "[" && !context.bracketQuote && context.prefix.length > 0); const rawLspItems = - context.kind === "bracket" + !includeLspItems ? [] : await getLanguageServerCompletions(context, doc, position, multilineBuffer, completionProvider); const lspItems = @@ -376,7 +379,14 @@ export async function collectCompletionEntries( 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 (columnFiltered.length > 0) { return dedupeCompletionEntries(columnFiltered); } From 320cfc3b73d50211f4b781638d4fd76ed0b4b5cb Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Sat, 23 May 2026 21:32:18 +1000 Subject: [PATCH 4/6] fix(console): complete chained data fields --- src/Language/completion.ts | 62 +++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Language/completion.ts b/src/Language/completion.ts index f573610..5076349 100644 --- a/src/Language/completion.ts +++ b/src/Language/completion.ts @@ -9,6 +9,7 @@ type CompletionContext = { operator?: "$" | "@"; bracketOperator?: "[" | "[["; bracketQuote?: "\"" | "'"; + chainedBracket?: boolean; functionName?: string; dataObjectName?: string; snapshotInput: string; @@ -110,6 +111,11 @@ 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", @@ -133,7 +139,7 @@ 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 { @@ -162,12 +168,15 @@ function getFieldInsertText(name: string, context: CompletionContext): string { } 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) { @@ -186,6 +195,9 @@ function detectDataContext(beforeCursor: string): string | undefined { if (/(\|>)\s*_\s*\[{1,2}[^\]]*$/.test(afterPipe)) { return firstPipeObject; } + if (/(\|>)\s*_\s*\[{1,2}[\s\S]*\]\$[a-zA-Z0-9._]*$/.test(afterPipe)) { + return firstPipeObject; + } } } @@ -232,24 +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 bracketOperator = bracketMatch[2] as "[" | "[["; const prefix = bracketMatch[4] || ""; const bracketObject = bracketMatch[1]; - const effectiveDataObject = (isPipePlaceholder(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[3] ? undefined : "[", - objectName: bracketObject, + objectName: baseObject, bracketOperator, bracketQuote: bracketMatch[3] ? bracketMatch[3] as "\"" | "'" : undefined, + chainedBracket: bracketObject !== baseObject || isPlaceholderBracket, dataObjectName: effectiveDataObject, operator: undefined, snapshotInput, @@ -265,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, }; @@ -374,6 +392,7 @@ export async function collectCompletionEntries( const fallbackBufferItems = filterShadowedBufferEntries(bufferItems, [ ...lspItems, ...sessionItems, + ...runtimeMemberItems, ...columnItems, ]); @@ -387,6 +406,9 @@ export async function collectCompletionEntries( 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); } @@ -424,10 +446,14 @@ 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); @@ -660,17 +686,21 @@ function getConsoleBufferCompletions( 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); @@ -681,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) ) { From 17a54652b86d00a3c20ad464f1792842167e30b0 Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Sun, 24 May 2026 00:33:13 +1000 Subject: [PATCH 5/6] Add configurable insertion shortcut of pipe operator for the console --- package.json | 26 ++++++++++++++++++++++++++ src/Terminal/rTerminal.ts | 15 +++++++++++++++ src/extension.ts | 38 ++++++++++++++++++++++++++++++++++---- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a7564ec..e7519b4 100644 --- a/package.json +++ b/package.json @@ -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/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/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( From 4e20bd6257260dd61803da80a84a0aff667fd8c4 Mon Sep 17 00:00:00 2001 From: "4111978+Fred-Wu@users.noreply.github.com" <4111978+Fred-Wu@users.noreply.github.com> Date: Sun, 24 May 2026 12:24:06 +1000 Subject: [PATCH 6/6] - Update README and CHANGELOG - Bump version to v0.2.7 --- CHANGELOG.md | 11 +++++++++++ README.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) 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 e7519b4..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",