Skip to content
Open
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
5 changes: 5 additions & 0 deletions extensions/positron-r/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@
"type": "boolean",
"default": false,
"description": "%r.configuration.workspaceSymbols.includeCommentSections.description%"
},
"positron.r.autoConvertFilePaths": {
"type": "boolean",
"default": true,
"description": "%r.configuration.autoConvertFilePaths.description%"
}
}
},
Expand Down
1 change: 1 addition & 0 deletions extensions/positron-r/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"r.configuration.diagnostics.enable.description": "Enable R diagnostics globally",
"r.configuration.symbols.includeAssignmentsInBlocks.description": "Controls whether assigned objects inside `{}` blocks are included as document symbols, in addition to top-level assignments. Reopen files or restart the server for the setting to take effect.",
"r.configuration.workspaceSymbols.includeCommentSections.description": "Controls whether comment sections like `# My section ---` are included as workspace symbols.",
"r.configuration.autoConvertFilePaths.description": "Automatically convert file paths when pasting files from file manager into R contexts. Converts backslashes (\\) to forward slashes (/), adds quotes, and formats multiple files as R vectors.",
"r.configuration.defaultRepositories.description": "The default repositories to use for R package installation, if no repository is otherwise specified in R startup scripts (restart Positron to apply).\n\nThe default repositories will be set as the `repos` option in R.",
"r.configuration.defaultRepositories.auto.description": "Automatically choose a default repository, or use a repos.conf file if it exists.",
"r.configuration.defaultRepositories.rstudio.description": "Use the RStudio CRAN mirror (cran.rstudio.com)",
Expand Down
4 changes: 4 additions & 0 deletions extensions/positron-r/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { registerUriHandler } from './uri-handler';
import { registerRLanguageModelTools } from './llm-tools.js';
import { registerFileAssociations } from './file-associations.js';
import { PositronSupervisorApi } from './positron-supervisor';
import { registerRFilePasteProvider } from './languageFeatures/rFilePasteProvider.js';

export const LOGGER = vscode.window.createOutputChannel('R Language Pack', { log: true });

Expand Down Expand Up @@ -43,6 +44,9 @@ export function activate(context: vscode.ExtensionContext) {
// Register file associations.
registerFileAssociations();

// Register R file paste provider.
registerRFilePasteProvider(context);

// Prepare to handle cli-produced hyperlinks that target the positron-r extension.
registerUriHandler();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# (Mostly Windows) File Path Auto-Conversion Feature
Copy link
Member Author

Choose a reason for hiding this comment

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

I will presumably remove this file when this feature is done, which may or may not happen in this PR. I.e. might decide to take the win in R documents and handle the console separately.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Typically we don't commit work-in-progress notes files to the repo -- if you want to preserve these, maybe file an issue and link to it from the relevant code?


## Overview

The intent of this feature is to replicate this RStudio behaviour: when pasting files (copied from the file manager, most especially Windows Explorer), they get converted into usable file paths. Where "usable" means `\` has been replaced by `/` and the whole path is wrapped in double quotes. (And I really do mean there are files on the clipboard, not just text that looks like a file path.)

Ideally, we would do this in R files AND in the R console (and potentially also in Python?). It turns out it's much easier to implement this in the editor, so that's where we're starting. These notes record some successful efforts to get this working in the console, but the implementation seemed yucky, so I've chosen to pause and get some advice before trying that again.

**Motivating GitHub Issue**: https://github.com/posit-dev/positron/issues/8393

These notes are partially authored by Claude and partially by @jennybc.

## Problem Statement

RStudio users expect Windows file paths to be automatically converted when pasting **files** (not text paths) into R contexts. This feature bridges the gap between file manager operations and data analysis workflows.

## Behavior

### What Gets Converted ✅
**File clipboard content** (copied from file manager):
- **Single file**: `"C:/Users/file.txt"`
- **Multiple files**: `c("C:/Users/file1.txt", "C:/Users/file2.txt")`
- **Files with spaces**: `"C:/Users/My Documents/file.txt"`
- **Files with quotes**: `"C:/Users/My \"Special\" File.txt"` (properly escaped). Note that double quotes aren't allowed in Windows file paths, so until I get back onto a macOS machine, I can't be sure that we even need to worry about this case.

### What Doesn't Get Converted ❌
- **UNC paths**: `\\server\share\file.txt` (skipped entirely for safety)
- **Text paths**: `C:\Users\file.txt` typed as text (not file clipboard)
- **Mixed scenarios**: If any UNC paths detected, skip conversion for all files
- **Non-R contexts**: Python console, other languages (unaffected)

### Key Features
- **RStudio compatible**: Goal is to (eventually) match `formatDesktopPath()` behavior exactly
- **Safe UNC handling**: Uses VS Code's `isUNC()` to detect and skip network paths
- **User controllable**: `positron.r.autoConvertFilePaths` setting (defaults enabled)
- **Platform agnostic**: Works on any OS, detects file clipboard via `text/uri-list`
- **Universal string formatting**: Quote escaping works for any programming language

## Architecture

**3-Layer Design** maintaining clean separation of concerns:

### 1. Core Layer (Language-Agnostic)
**File**: `src/vs/workbench/contrib/positronPathUtils/common/filePathConverter.ts`

Key exported function is `convertClipboardFiles()`.

Unit tests in `src/vs/workbench/contrib/positronPathUtils/test/browser/filePathConverter.test.ts`

### 2. API Layer (Official Positron Extension API)
**Files**: `src/positron-dts/positron.d.ts` + `src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts`

Exposes `positron.paths.extractClipboardFilePaths()` for extensions to use.

### 3. Language Layer (only in positron-r for now)
**File**: `extensions/positron-r/src/languageFeatures/rFilePasteProvider.ts`

`provideDocumentPasteEdits()` checks the user setting `positron.r.autoConvertFilePaths` and whether `positron.paths.extractClipboardFilePaths()` has any paths to provide.

If not, early return of `undefined` and default paste behaviour takes over.

If so, the converted file paths are potentially formatted for use in R, e.g. inside `c(...)` for multiple files.

`registerRFilePasteProvider()` is used in `extensions/positron-r/src/extension.ts` to register the provider via `vscode.languages.registerDocumentPasteEditProvider()`. This is the missing piece for the console. What's the best equivalent implementation there?

## Implementation Summary

### ✅ Status: R Files Complete & Tested | Console Deferred

**Key Features Delivered:**
- **RStudio compatibility**: Exact `formatDesktopPath()` behavior match
- **UNC path safety**: Uses VS Code's `isUNC()` for robust network path detection
- **R files support**: Full implementation working in R source files
- **Language-agnostic core**: Ready for future Python/Julia extensions
- **User controllable**: `positron.r.autoConvertFilePaths` setting (defaults enabled)

**Testing Validation:**
- ✅ **Unit tests**: comprehensive test cases covering all scenarios
- ✅ **Manual testing**: Confirmed working in R files - produces `"c:/Users/jenny/readxl/inst/extdata/datasets.xlsx"`
- ✅ **Build verification**: Clean TypeScript compilation
- ✅ **UNC safety**: Network paths properly detected and skipped

### 📋 Console Status: Attempted, Learned From, Removed

**What We Learned About Console Architecture:**
- **Console is not a document**: Uses "simple widget"/mini editor architecture, so `DocumentPasteEditProvider` doesn't work
- **Different paste handling**: Console has its own paste logic in `positronConsole.contribution.ts` that only calls `clipboardService.readText()`
- **Architecture challenges**: We (Claude and @jennybc) did get this working, but it didn't feel well-designed. In particular, R-specific stuff was appearing Positron core. How to make this behaviour something that a language pack can contribute?

**Decision**: Console implementation was **attempted, learned from, and removed**. Shipping with R files support only.

**Future Console Work:**
For future console implementation, investigate:
1. **Root cause**: Why enhanced console paste handler didn't trigger
2. **Alternative approaches**: Direct Monaco editor integration, different paste event handling

## Testing/Debugging

**To see the paste provider title**: Copy files from file manager, then use **"Paste As"** from the Command Palette (Ctrl+Shift+P) in an R file. This shows the picker with "Insert quoted, forward-slash file path(s)" option. NOTE: this is no longer working, once we pared things way back. Hopefully we can get it back for the console implementation.

**To run a small set of unit tests**: Do something like: `scripts/test.sh --grep "UNC path"`

**To manually test UNC paths**:

* Open Windows Explorer
* Type `\\localhost\c$` and press Enter
* Navigate to any file and copy it
* Paste into an R file in Positron and (hopefully) observe no conversion

Example: `\\localhost\c$\Users\jenny\readxl\inst\extdata\geometry.xlsx`

## Interesting code to study

The markdown-language-features extension has a similar feature for pasting or dragging-and-dropping of files into markdown documents. I will look at this later. Key files:

* `extensions\markdown-language-features\src\languageFeatures\copyFiles\dropOrPasteResource.ts`
* `extensions\markdown-language-features\src\extension.shared.ts`
* `extensions\markdown-language-features\src\languageFeatures\copyFiles\shared.ts` (look at `getRelativeMdPath()`)

Other potentially interesting files:
* `src\vs\editor\contrib\dropOrPasteInto\browser\defaultProviders.ts`

## RStudio's implementation

* `onDesktopPaste()`: <https://github.com/rstudio/rstudio/blob/5364b4eb3fd7333c15b5e637007bf93d48963c50/src/gwt/src/org/rstudio/studio/client/workbench/views/source/editors/text/AceEditorWidget.java#L428-L490>
* `makeProjectRelative()`: <https://github.com/rstudio/rstudio/blob/4f7258ad7728bca57e8635c9011f351801620e22/src/cpp/session/modules/SessionFiles.cpp#L781-L815>
* `FilePath::createAliasedPath()`: <https://github.com/rstudio/rstudio/blob/5364b4eb3fd7333c15b5e637007bf93d48963c50/src/cpp/shared_core/FilePath.cpp#L444-L472>
73 changes: 73 additions & 0 deletions extensions/positron-r/src/languageFeatures/rFilePasteProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import * as positron from 'positron';
import * as os from 'os';

/**
* Document paste edit provider for R files that converts files on the clipboard
* into file paths that are usable in R code.
*/
export class RFilePasteProvider implements vscode.DocumentPasteEditProvider {

/**
* Provide paste edits for R filepaths when files are detected on clipboard.
*/
async provideDocumentPasteEdits(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
context: vscode.DocumentPasteEditContext,
token: vscode.CancellationToken
): Promise<vscode.DocumentPasteEdit[] | undefined> {

const setting = vscode.workspace.getConfiguration('positron.r').get<boolean>('autoConvertFilePaths');
Copy link
Member Author

Choose a reason for hiding this comment

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

Do we even need a setting to allow people to opt out? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

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

Need? No. But I think it's fine to leave in until we're confident that this won't get in anyone's way!

if (!setting) {
return undefined;
}

const filePaths = await positron.paths.extractClipboardFilePaths(dataTransfer, {
preferRelative: true,
homeUri: vscode.Uri.file(os.homedir())
});

if (!filePaths) {
return undefined;
}

// Format for R: single path or R vector syntax
const insertText = filePaths.length === 1
? filePaths[0]
: `c(${filePaths.join(', ')})`;

// TODO @jennybc: at some point, this sort of special paste WAS showing
// up when using the "Paste As..." command. Now it doesn't. Presumably
// had something to with the code I've since ripped out. Would be nice
// to get that back.
return [{
insertText,
title: 'Insert file path(s)',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this text be localized?

Copy link
Member Author

Choose a reason for hiding this comment

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

At the moment, I don't think this text is ever surfaced 🫠 At some point, when this PR "did more" but also had a weaker design, this text was surfaced (in a "Paste As" sort of experience). I will rationalize this bit one way or another before I merge.

kind: vscode.DocumentDropOrPasteEditKind.Text
}];
}
}

/**
* Register the R file paste provider for automatic file path conversion.
*/
export function registerRFilePasteProvider(context: vscode.ExtensionContext): void {
const rFilePasteProvider = new RFilePasteProvider();
context.subscriptions.push(
vscode.languages.registerDocumentPasteEditProvider(
{ language: 'r' },
rFilePasteProvider,
{
pasteMimeTypes: ['text/uri-list'],
providedPasteEditKinds: [vscode.DocumentDropOrPasteEditKind.Text]
}
)
);
}
44 changes: 44 additions & 0 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,50 @@ declare module 'positron' {
export function registerConnectionDriver(driver: ConnectionsDriver): vscode.Disposable;
}

/**
* File path utilities for data analysis code.
*/
namespace paths {
Copy link
Member Author

Choose a reason for hiding this comment

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

I added this to the positron API to centralize the initial futzing with the clipboard and file URIs, which would be needed to produce usable file paths for any language (e.g. R or Python).

/**
* Options for extracting clipboard file paths
*/
export interface ExtractClipboardFilePathsOptions {
/**
* Whether to prefer relative paths when workspace context is available.
* Defaults to true.
*/
preferRelative?: boolean;

/**
* Custom base URI for relative path calculation.
* If not provided and preferRelative is true, uses the first workspace folder.
*/
baseUri?: vscode.Uri;

/**
* User home directory URI for home-relative path calculation.
*/
homeUri?: vscode.Uri;
}

/**
* Extract file paths from clipboard for use in data analysis code.
Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO there's no need to specify "for use in data analysis code?" This API is very generic

* Detects files copied from file manager and returns their paths for use in scripts.
* Main motivation is the Windows issue of replacing `\` with `/`.
* Also returns paths relative to the workspace folder, when possible.
* Try to use core utilities (versus DIY path hacking).

* @param dataTransfer The clipboard data transfer object
* @param options Options for path conversion
* @returns A Thenable that resolves to an array of quoted, forward-slash,
* possibly relative file paths, or null if no files detected
*/
export function extractClipboardFilePaths(
dataTransfer: vscode.DataTransfer,
options?: ExtractClipboardFilePathsOptions
): Thenable<string[] | null>;
}

/**
* Experimental AI features.
*/
Expand Down
52 changes: 52 additions & 0 deletions src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { ExtHostLanguageRuntime } from './extHostLanguageRuntime.js';
import type * as positron from 'positron';
import type * as vscode from 'vscode';
import { URI } from '../../../../base/common/uri.js';
import { IExtHostRpcService } from '../extHostRpcService.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
Expand Down Expand Up @@ -35,6 +36,7 @@ import { ExtHostAiFeatures } from './extHostAiFeatures.js';
import { IToolInvocationContext } from '../../../contrib/chat/common/languageModelToolsService.js';
import { IPositronLanguageModelSource } from '../../../contrib/positronAssistant/common/interfaces/positronAssistantService.js';
import { ExtHostEnvironment } from './extHostEnvironment.js';
import { convertClipboardFiles } from '../../../contrib/positronPathUtils/common/filePathConverter.js';
import { ExtHostPlotsService } from './extHostPlotsService.js';

/**
Expand Down Expand Up @@ -264,6 +266,55 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce
}
};

const paths: typeof positron.paths = {
/**
* Extract file paths from clipboard for use in data analysis code.
*/
async extractClipboardFilePaths(
dataTransfer: vscode.DataTransfer,
options?: {
preferRelative?: boolean;
baseUri?: vscode.Uri;
homeUri?: vscode.Uri;
}
): Promise<string[] | null> {
// Get URI list data from VS Code DataTransfer
const uriListItem = dataTransfer.get('text/uri-list');
if (!uriListItem) {
return null;
}

try {
const uriListData = await uriListItem.asString();
if (!uriListData) {
return null;
}

// Provide workspace fallback if no baseUri specified
let resolvedOptions = options;
if (options?.preferRelative && !options.baseUri) {
const workspaceFolders = extHostWorkspace.getWorkspaceFolders();
if (workspaceFolders && workspaceFolders.length > 0) {
resolvedOptions = {
...options,
baseUri: workspaceFolders[0].uri
};
}
}

const convertOptions = resolvedOptions ? {
...resolvedOptions,
baseUri: resolvedOptions.baseUri ? URI.from(resolvedOptions.baseUri) : undefined,
homeUri: resolvedOptions.homeUri ? URI.from(resolvedOptions.homeUri) : undefined
} : undefined;

return convertClipboardFiles(uriListData, convertOptions);
} catch {
return null;
}
}
};

const ai: typeof positron.ai = {
getCurrentPlotUri(): Thenable<string | undefined> {
return extHostAiFeatures.getCurrentPlotUri();
Expand Down Expand Up @@ -317,6 +368,7 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce
languages,
methods,
environment,
paths,
connections,
ai,
CodeAttributionSource: extHostTypes.CodeAttributionSource,
Expand Down
Loading