Skip to content

Do not break completion for unsupported FS providers #5536

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions extensions/vscode/src/VsCodeIde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@

async fileExists(uri: string): Promise<boolean> {
try {
await vscode.workspace.fs.stat(vscode.Uri.parse(uri));
return true;
const stat = await this.ideUtils.stat(vscode.Uri.parse(uri));
return stat !== null;
} catch (error) {
if (error instanceof vscode.FileSystemError) {
return false;
Expand Down Expand Up @@ -137,7 +137,7 @@
// But don't wait to return this immediately
// We will use a callback to refresh the config
if (!this.askedForAuth) {
vscode.window

Check warning on line 140 in extensions/vscode/src/VsCodeIde.ts

View workflow job for this annotation

GitHub Actions / vscode-checks

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
.showInformationMessage(
"Continue will request read access to your GitHub email so that we can prevent abuse of the free trial. If you prefer not to sign in, you can use Continue with your own API keys or local model.",
"Sign in",
Expand All @@ -149,7 +149,7 @@
await vscode.commands.executeCommand(
"continue.continueGUIView.focus",
);
(await this.vscodeWebviewProtocolPromise).request(

Check warning on line 152 in extensions/vscode/src/VsCodeIde.ts

View workflow job for this annotation

GitHub Actions / vscode-checks

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
"openOnboardingCard",
undefined,
);
Expand Down Expand Up @@ -189,7 +189,7 @@
},
);
} else if (selection === "Learn more") {
vscode.env.openExternal(

Check warning on line 192 in extensions/vscode/src/VsCodeIde.ts

View workflow job for this annotation

GitHub Actions / vscode-checks

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
vscode.Uri.parse(
"https://docs.continue.dev/reference/Model%20Providers/freetrial",
),
Expand Down Expand Up @@ -221,7 +221,7 @@
} else if (!this.askedForAuth) {
// User cancelled the login prompt
// Explain that they can avoid the prompt by removing free trial models from config.json
vscode.window

Check warning on line 224 in extensions/vscode/src/VsCodeIde.ts

View workflow job for this annotation

GitHub Actions / vscode-checks

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
.showInformationMessage(
"We'll only ask you to log in if using the free trial. To avoid this prompt, make sure to remove free trial models from your config.json",
"Remove for me",
Expand Down Expand Up @@ -335,10 +335,10 @@
const pathToLastModified: FileStatsMap = {};
await Promise.all(
files.map(async (file) => {
const stat = await vscode.workspace.fs.stat(vscode.Uri.parse(file));
const stat = await this.ideUtils.stat(vscode.Uri.parse(file), false /* No need to catch ENOPRO exceptions */);
pathToLastModified[file] = {
lastModified: stat.mtime,
size: stat.size,
lastModified: stat!.mtime,
size: stat!.size,
};
}),
);
Expand Down Expand Up @@ -400,7 +400,10 @@
vscode.workspace.workspaceFolders?.map((folder) => folder.uri) || [];
const configs: ContinueRcJson[] = [];
for (const workspaceDir of workspaceDirs) {
const files = await vscode.workspace.fs.readDirectory(workspaceDir);
const files = await this.ideUtils.readDirectory(workspaceDir);
if (files === null) {//Unlikely, but just in case...
continue;
}
for (const [filename, type] of files) {
if (
(type === vscode.FileType.File ||
Expand Down Expand Up @@ -512,12 +515,15 @@
return openTextDocument.getText();
}

const fileStats = await vscode.workspace.fs.stat(uri);
if (fileStats.size > 10 * VsCodeIde.MAX_BYTES) {
const fileStats = await this.ideUtils.stat(uri);
if (fileStats === null || fileStats.size > 10 * VsCodeIde.MAX_BYTES) {
return "";
}

const bytes = await vscode.workspace.fs.readFile(uri);
const bytes = await this.ideUtils.readFile(uri);
if (bytes === null) {
return "";
}

// Truncate the buffer to the first MAX_BYTES
const truncatedBytes = bytes.slice(0, VsCodeIde.MAX_BYTES);
Expand Down Expand Up @@ -599,7 +605,10 @@

const ignoreGlobs: Set<string> = new Set();
for (const file of ignoreFiles) {
const content = await vscode.workspace.fs.readFile(file);
const content = await this.ideUtils.readFile(file);
if (content === null) {
continue;
}
const filePath = vscode.workspace.asRelativePath(file);
const fileDir = filePath
.replace(/\\/g, "/")
Expand Down Expand Up @@ -743,7 +752,8 @@
}

async listDir(dir: string): Promise<[string, FileType][]> {
return vscode.workspace.fs.readDirectory(vscode.Uri.parse(dir)) as any;
const entries = await this.ideUtils.readDirectory(vscode.Uri.parse(dir));
return entries === null? [] : entries as any;
}

private getIdeSettingsSync(): IdeSettings {
Expand Down
9 changes: 5 additions & 4 deletions extensions/vscode/src/autocomplete/completionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as vscode from "vscode";

import { handleLLMError } from "../util/errorHandling";
import { showFreeTrialLoginMessage } from "../util/messages";
import { VsCodeIde } from "../VsCodeIde";
import { VsCodeWebviewProtocol } from "../webviewProtocol";

import { getDefinitionsFromLsp } from "./lsp";
Expand All @@ -23,8 +24,6 @@ import {
stopStatusBarLoading,
} from "./statusBar";

import type { IDE } from "core";

interface VsCodeCompletionInput {
document: vscode.TextDocument;
position: vscode.Position;
Expand Down Expand Up @@ -61,13 +60,15 @@ export class ContinueCompletionProvider

private completionProvider: CompletionProvider;
private recentlyVisitedRanges: RecentlyVisitedRangesService;
private recentlyEditedTracker = new RecentlyEditedTracker();
private recentlyEditedTracker: RecentlyEditedTracker;

constructor(
private readonly configHandler: ConfigHandler,
private readonly ide: IDE,
private readonly ide: VsCodeIde,
private readonly webviewProtocol: VsCodeWebviewProtocol,
) {
this.recentlyEditedTracker = new RecentlyEditedTracker(ide.ideUtils);

async function getAutocompleteModel() {
const { config } = await configHandler.loadConfig();
if (!config) {
Expand Down
14 changes: 9 additions & 5 deletions extensions/vscode/src/autocomplete/recentlyEdited.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getSymbolsForSnippet } from "core/autocomplete/context/ranking";
import { RecentlyEditedRange } from "core/autocomplete/util/types";
import * as vscode from "vscode";

import { VsCodeIdeUtils } from "../util/ideUtils";

type VsCodeRecentlyEditedRange = {
uri: vscode.Uri;
range: vscode.Range;
Expand All @@ -21,7 +23,7 @@ export class RecentlyEditedTracker {
private recentlyEditedDocuments: VsCodeRecentlyEditedDocument[] = [];
private static maxRecentlyEditedDocuments = 10;

constructor() {
constructor(private ideUtils: VsCodeIdeUtils) {
vscode.workspace.onDidChangeTextDocument((event) => {
event.contentChanges.forEach((change) => {
const editedRange = {
Expand Down Expand Up @@ -112,13 +114,15 @@ export class RecentlyEditedTracker {
private async _getContentsForRange(
entry: Omit<VsCodeRecentlyEditedRange, "lines" | "symbols">,
): Promise<string> {
return vscode.workspace.fs.readFile(entry.uri).then((content) =>
content
const content = await this.ideUtils.readFile(entry.uri);
if (content === null) {
return "";
}
return content
.toString()
.split("\n")
.slice(entry.range.start.line, entry.range.end.line + 1)
.join("\n"),
);
.join("\n");
}

public async getRecentlyEditedRanges(): Promise<RecentlyEditedRange[]> {
Expand Down
5 changes: 3 additions & 2 deletions extensions/vscode/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,13 +570,14 @@ const getCommandsMap: (
for await (const fileUri of walkDirAsync(uri.toString(), ide, {
source: "vscode continue.selectFilesAsContext command",
})) {
addEntireFileToContext(
await addEntireFileToContext(
vscode.Uri.parse(fileUri),
sidebar.webviewProtocol,
ide.ideUtils
);
}
} else {
addEntireFileToContext(uri, sidebar.webviewProtocol);
await addEntireFileToContext(uri, sidebar.webviewProtocol, ide.ideUtils);
}
}
},
Expand Down
14 changes: 10 additions & 4 deletions extensions/vscode/src/util/addCode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { RangeInFileWithContents } from "core";
import * as os from "node:os";

import { RangeInFileWithContents } from "core";
import * as vscode from "vscode";

import { VsCodeIdeUtils } from "./ideUtils";

import type { VsCodeWebviewProtocol } from "../webviewProtocol";

export function getRangeInFileWithContents(
Expand Down Expand Up @@ -85,16 +89,18 @@ export async function addHighlightedCodeToContext(
export async function addEntireFileToContext(
uri: vscode.Uri,
webviewProtocol: VsCodeWebviewProtocol | undefined,
ideUtils: VsCodeIdeUtils
) {
// If a directory, add all files in the directory
const stat = await vscode.workspace.fs.stat(uri);
if (stat.type === vscode.FileType.Directory) {
const files = await vscode.workspace.fs.readDirectory(uri);
const stat = await ideUtils.stat(uri);
if (stat?.type === vscode.FileType.Directory) {
const files = (await ideUtils.readDirectory(uri))!;//files can't be null if we reached this point
for (const [filename, type] of files) {
if (type === vscode.FileType.File) {
addEntireFileToContext(
vscode.Uri.joinPath(uri, filename),
webviewProtocol,
ideUtils
);
}
}
Expand Down
123 changes: 115 additions & 8 deletions extensions/vscode/src/util/ideUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { EXTENSION_NAME } from "core/control-plane/env";
import { findUriInDirs } from "core/util/uri";
import _ from "lodash";
import * as vscode from "vscode";
import * as URI from "uri-js";
import * as vscode from "vscode";

import { threadStopped } from "../debug/debug";
import { VsCodeExtension } from "../extension/VsCodeExtension";
import { GitExtension, Repository } from "../otherExtensions/git";
Expand All @@ -14,12 +16,14 @@ import {

import { getUniqueId, openEditorAndRevealRange } from "./vscode";

import type { Range, RangeInFile, Thread } from "core";
import { findUriInDirs } from "core/util/uri";
import type { Range, Thread } from "core";

const util = require("node:util");
const asyncExec = util.promisify(require("node:child_process").exec);

const NO_FS_PROVIDER_ERROR = "ENOPRO";
const UNSUPPORTED_SCHEMES: Set<string> = new Set();

export class VsCodeIdeUtils {
visibleMessages: Set<string> = new Set();

Expand Down Expand Up @@ -99,13 +103,114 @@ export class VsCodeIdeUtils {

async fileExists(uri: vscode.Uri): Promise<boolean> {
try {
await vscode.workspace.fs.stat(uri);
return true;
return (await this.stat(uri)) !== null;
} catch {
return false;
}
}

/**
* Read the entire contents of a file from the given URI.
*
* @param uri - The URI of the file to read.
* @param ignoreMissingProviders - Optional flag to ignore missing file system providers for unsupported schemes.
* Defaults to `true`.
* @returns A promise that resolves to the file content as a `Uint8Array`, or `null` if the scheme is unsupported
* or the provider is missing and `ignoreMissingProviders` is `true`.
* If `ignoreMissingProviders` is `false`, it will throw an error for unsupported schemes or missing providers.
* @throws Will rethrow any error that is not related to missing providers or unsupported schemes.
*/
async readFile(
uri: vscode.Uri,
ignoreMissingProviders: boolean = true,
): Promise<Uint8Array | null> {
return await this.fsOperation(
uri,
async (u) => {
return await vscode.workspace.fs.readFile(u);
},
ignoreMissingProviders,
);
}

/**
* Retrieve metadata about a file from the given URI.
*
* @param uri - The URI of the file or directory to retrieve metadata about.
* @param ignoreMissingProviders - Optional. If `true`, missing file system providers will be ignored. Defaults to `true`.
* @returns A promise that resolves to a `vscode.FileStat` object containing the file metadata,
* or `null` if the scheme is unsupported or the provider is missing and `ignoreMissingProviders` is `true`.
*/
async stat(
uri: vscode.Uri,
ignoreMissingProviders: boolean = true,
): Promise<vscode.FileStat | null> {
return await this.fsOperation(
uri,
async (u) => {
return await vscode.workspace.fs.stat(uri);
},
ignoreMissingProviders,
);
}

/**
* Retrieve all entries of a directory from the given URI.
*
* @param uri - The URI of the directory to read.
* @param ignoreMissingProviders - Optional. If `true`, missing file system providers will be ignored. Defaults to `true`.
* @returns A promise that resolves to an array of tuples, where each tuple contains the name of a directory entry
* and its type (`vscode.FileType`), or `null` if the scheme is unsupported or the provider is missing and `ignoreMissingProviders` is `true`.
*/
async readDirectory(
uri: vscode.Uri,
ignoreMissingProviders: boolean = true,
): Promise<[string, vscode.FileType][] | null> {
return await this.fsOperation(
uri,
async (u) => {
return await vscode.workspace.fs.readDirectory(uri);
},
ignoreMissingProviders,
);
}

/**
* Performs a file system operation on the given URI using the provided delegate function.
*
* @template T The type of the result returned by the delegate function.
* @param uri The URI on which the file system operation is to be performed.
* @param delegate A function that performs the desired operation on the given URI.
* @param ignoreMissingProviders Whether to ignore errors caused by missing file system providers. Defaults to `true`.
* @returns A promise that resolves to the result of the delegate function, or `null` if the operation is skipped due to unsupported schemes or missing providers.
* @throws Re-throws any error encountered during the operation, except for missing provider errors when `ignoreMissingProviders` is `true`.
*/
private async fsOperation<T>(
uri: vscode.Uri,
delegate: (uri: vscode.Uri) => T,
ignoreMissingProviders: boolean = true,
): Promise<T | null> {
const scheme = uri.scheme;
if (ignoreMissingProviders && UNSUPPORTED_SCHEMES.has(scheme)) {
return null;
}
try {
return await delegate(uri);
} catch (err: any) {
if (
ignoreMissingProviders &&
//see https://github.com/microsoft/vscode/blob/c9c54f9e775e5f57d97bef796797b5bc670c8150/src/vs/workbench/api/common/extHostFileSystemConsumer.ts#L230
(err.name === NO_FS_PROVIDER_ERROR ||
err.message?.includes(NO_FS_PROVIDER_ERROR))
) {
UNSUPPORTED_SCHEMES.add(scheme);
console.log(`Ignoring missing provider error:`, err.message);
return null;
}
throw err;
}
}

showVirtualFile(name: string, contents: string) {
vscode.workspace
.openTextDocument(
Expand Down Expand Up @@ -185,9 +290,11 @@ export class VsCodeIdeUtils {
}

async readRangeInFile(uri: vscode.Uri, range: vscode.Range): Promise<string> {
const contents = new TextDecoder().decode(
await vscode.workspace.fs.readFile(uri),
);
const buffer = await this.readFile(uri);
if (buffer === null) {
return '';
}
const contents = new TextDecoder().decode(buffer);
const lines = contents.split("\n");
return `${lines
.slice(range.start.line, range.end.line)
Expand Down
Loading