From d0e4f81b103f601a0f04f2179ec5edcaa9008a5b Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Tue, 21 Jul 2026 22:15:26 +0200 Subject: [PATCH 1/3] Add support for workspace read requests --- client/src/common/client.ts | 6 +- client/src/common/codeConverter.ts | 46 +++- client/src/common/fileSystem.ts | 138 ++++++++++ protocol/metaModel.json | 295 +++++++++++++++++++++ protocol/src/common/protocol.fileSystem.ts | 202 ++++++++++++++ protocol/src/common/protocol.ts | 16 +- server/src/common/fileSystem.ts | 60 +++++ server/src/common/server.ts | 5 +- testbed/client/src/extension.ts | 26 ++ testbed/package.json | 12 + testbed/server/src/server.ts | 38 ++- 11 files changed, 837 insertions(+), 7 deletions(-) create mode 100644 client/src/common/fileSystem.ts create mode 100644 protocol/src/common/protocol.fileSystem.ts create mode 100644 server/src/common/fileSystem.ts diff --git a/client/src/common/client.ts b/client/src/common/client.ts index a5953c6ab..0f19ca602 100644 --- a/client/src/common/client.ts +++ b/client/src/common/client.ts @@ -95,6 +95,7 @@ import { InlineCompletionItemFeature, InlineCompletionMiddleware } from './inlin import { TextDocumentContentFeature, type TextDocumentContentMiddleware, type TextDocumentContentProviderShape } from './textDocumentContent'; import { FileSystemWatcherFeature } from './fileSystemWatcher'; import { ProgressFeature } from './progress'; +import { FileSystemFeature, FileSystemMiddleware } from './fileSystem'; /** * Controls when the output channel is revealed. @@ -291,7 +292,7 @@ type _WorkspaceMiddleware = { handleApplyEdit?: (this: void, params: ApplyWorkspaceEditParams, next: ApplyWorkspaceEditRequest.HandlerSignature) => HandlerResult; }; -export type WorkspaceMiddleware = _WorkspaceMiddleware & ConfigurationMiddleware & DidChangeConfigurationMiddleware & WorkspaceFolderMiddleware & FileOperationsMiddleware; +export type WorkspaceMiddleware = _WorkspaceMiddleware & ConfigurationMiddleware & DidChangeConfigurationMiddleware & WorkspaceFolderMiddleware & FileOperationsMiddleware & FileSystemMiddleware; interface _WindowMiddleware { showDocument?: ShowDocumentRequest.MiddlewareSignature; @@ -342,7 +343,7 @@ DocumentHighlightMiddleware & DocumentSymbolMiddleware & WorkspaceSymbolMiddlewa ColorProviderMiddleware & CodeActionMiddleware & CodeLensMiddleware & FormattingMiddleware & RenameMiddleware & DocumentLinkMiddleware & ExecuteCommandMiddleware & FoldingRangeProviderMiddleware & DeclarationMiddleware & SelectionRangeProviderMiddleware & CallHierarchyMiddleware & SemanticTokensMiddleware & LinkedEditingRangeMiddleware & TypeHierarchyMiddleware & InlineValueMiddleware & InlayHintsMiddleware & NotebookDocumentMiddleware & DiagnosticProviderMiddleware & -InlineCompletionMiddleware & TextDocumentContentMiddleware & GeneralMiddleware; +InlineCompletionMiddleware & TextDocumentContentMiddleware & FileSystemMiddleware & GeneralMiddleware; export type LanguageClientOptions = { documentSelector?: DocumentSelector | string[]; @@ -2085,6 +2086,7 @@ export abstract class BaseLanguageClient implements FeatureClient; +} + +export interface FileSystemStatSignature { + (this: void, uri: vscode.Uri): Promise; +} + +export interface FileSystemReadDirectorySignature { + (this: void, uri: vscode.Uri): Promise<[string, vscode.FileType][] | null>; +} + +/** + * File system middleware. + * + * @since 3.19.0 + */ +export interface FileSystemMiddleware { + fs?: { + stat?: (this: void, uri: vscode.Uri, next: FileSystemStatSignature) => vscode.ProviderResult; + readFile?: (this: void, uri: vscode.Uri, encoding: string | undefined, next: FileSystemReadFileSignature) => vscode.ProviderResult; + readDirectory?: (this: void, uri: vscode.Uri, next: FileSystemReadDirectorySignature) => vscode.ProviderResult<[string, vscode.FileType][] | null>; + }; +} + +interface WorkspaceFileSystemMiddleware { + workspace?: FileSystemMiddleware; +} + +// TextDecoder is available in all supported environments, but we can't use it directly +// because that would require us to use the dom or webworker lib, which we don't want to do. +// So we declare it here to make TypeScript happy. +declare class TextDecoder { + constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean }); + decode(input?: Uint8Array): string; +} + +/** + * file system feature. From server to client. + */ +export class FileSystemFeature implements StaticFeature { + + private readonly _client: FeatureClient; + + constructor(client: FeatureClient) { + this._client = client; + } + + getState(): FeatureState { + return { kind: 'static' }; + } + + public fillClientCapabilities(capabilities: ClientCapabilities): void { + capabilities.workspace ??= {}; + capabilities.workspace.fileSystem ??= {}; + capabilities.workspace.fileSystem.stat = true; + capabilities.workspace.fileSystem.readFile = true; + capabilities.workspace.fileSystem.readDirectory = true; + } + + public initialize(): void { + const client = this._client; + client.onRequest(StatRequest.type, async (params) => { + const paramsUri = this._client.protocol2CodeConverter.asUri(params.uri); + const fileStat: FileSystemStatSignature = async (uri) => { + try { + const vstat = await vscode.workspace.fs.stat(uri); + return vstat; + } catch { + return null; + } + }; + const middleware = client.middleware.workspace; + const result = await (middleware?.fs?.stat + ? middleware.fs.stat(paramsUri, fileStat) + : fileStat(paramsUri)); + return result + ? this._client.code2ProtocolConverter.asFileStat(result) + : null; + }); + client.onRequest(ReadFileRequest.type, async (params) => { + const paramsUri = this._client.protocol2CodeConverter.asUri(params.uri); + const fileRead: FileSystemReadFileSignature = async (uri, encoding) => { + try { + const bytes = await vscode.workspace.fs.readFile(uri); + const decoder = new TextDecoder(encoding || 'utf-8'); + return decoder.decode(bytes); + } catch { + return null; + } + }; + const middleware = client.middleware.workspace; + const result = await (middleware?.fs?.readFile + ? middleware.fs.readFile(paramsUri, params.encoding, fileRead) + : fileRead(paramsUri, params.encoding)); + if (result === undefined || result === null) { + return null; + } + return { text: result }; + }); + client.onRequest(ReadDirectoryRequest.type, async (params) => { + const paramsUri = this._client.protocol2CodeConverter.asUri(params.uri); + const directoryRead: FileSystemReadDirectorySignature = async (uri) => { + try { + const entries = await vscode.workspace.fs.readDirectory(uri); + return entries; + } catch { + return null; + } + }; + const middleware = client.middleware.workspace; + const result = await (middleware?.fs?.readDirectory + ? middleware.fs.readDirectory(paramsUri, directoryRead) + : directoryRead(paramsUri)); + if (result === undefined || result === null) { + return null; + } + const entries = result.map(this._client.code2ProtocolConverter.asDirectoryEntry); + return entries; + }); + } + + public clear(): void { + } +} diff --git a/protocol/metaModel.json b/protocol/metaModel.json index fc68d8d48..d6f36d984 100644 --- a/protocol/metaModel.json +++ b/protocol/metaModel.json @@ -1149,6 +1149,84 @@ "documentation": "The `workspace/textDocumentContent` request is sent from the server to the client to refresh\nthe content of a specific text document.\n\n@since 3.18.0", "since": "3.18.0" }, + { + "method": "workspace/stat", + "typeName": "StatRequest", + "result": { + "kind": "or", + "items": [ + { + "kind": "reference", + "name": "FileStat" + }, + { + "kind": "base", + "name": "null" + } + ] + }, + "messageDirection": "serverToClient", + "clientCapability": "workspace.fileOperations.fileStat", + "params": { + "kind": "reference", + "name": "StatParams" + }, + "documentation": "The stat request is sent from the server to the client to get metadata about a file.\n\nThe request can return a `FileStat` which will be used to determine the type of the file,\nits size, and the creation and modification time. Returns `null` if the file does not exist.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "method": "workspace/readDirectory", + "typeName": "ReadDirectoryRequest", + "result": { + "kind": "or", + "items": [ + { + "kind": "array", + "element": { + "kind": "reference", + "name": "DirectoryEntry" + } + }, + { + "kind": "base", + "name": "null" + } + ] + }, + "messageDirection": "serverToClient", + "clientCapability": "workspace.fileOperations.readDirectory", + "params": { + "kind": "reference", + "name": "ReadDirectoryParams" + }, + "documentation": "The read directory request is sent from the server to the client to get the entries of a directory.\n\nThe request can return a `DirectoryEntry[]` which contains the directory entries.\nReturns `null` if the directory does not exist or the client cannot read it.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "method": "workspace/readFile", + "typeName": "ReadFileRequest", + "result": { + "kind": "or", + "items": [ + { + "kind": "reference", + "name": "ReadFileResult" + }, + { + "kind": "base", + "name": "null" + } + ] + }, + "messageDirection": "serverToClient", + "clientCapability": "workspace.fileOperations.readFile", + "params": { + "kind": "reference", + "name": "ReadFileParams" + }, + "documentation": "The read file request is sent from the server to the client to get the content of a file.\n\nThe request can return a `ReadFileResult` which contains the content of the file.\nReturns `null` if the file does not exist or the client cannot read it.\n\n@since 3.19.0", + "since": "3.19.0" + }, { "method": "client/registerCapability", "typeName": "RegistrationRequest", @@ -4517,6 +4595,153 @@ "documentation": "Parameters for the `workspace/textDocumentContent/refresh` request.\n\n@since 3.18.0", "since": "3.18.0" }, + { + "name": "StatParams", + "properties": [ + { + "name": "uri", + "type": { + "kind": "base", + "name": "DocumentUri" + }, + "documentation": "A URI for the location of the file/folder." + } + ], + "documentation": "The parameters sent in a request to get metadata about a file.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "name": "FileStat", + "properties": [ + { + "name": "type", + "type": { + "kind": "reference", + "name": "FileType" + }, + "documentation": "The type of the file, e.g. is a regular file or a directory." + }, + { + "name": "isSymlink", + "type": { + "kind": "base", + "name": "boolean" + }, + "documentation": "Whether the file is a symbolic link." + }, + { + "name": "ctime", + "type": { + "kind": "base", + "name": "integer" + }, + "documentation": "The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC." + }, + { + "name": "mtime", + "type": { + "kind": "base", + "name": "integer" + }, + "documentation": "The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC." + }, + { + "name": "size", + "type": { + "kind": "base", + "name": "integer" + }, + "documentation": "The size in bytes." + } + ], + "documentation": "Represents metadata about a file.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "name": "ReadDirectoryParams", + "properties": [ + { + "name": "uri", + "type": { + "kind": "base", + "name": "DocumentUri" + }, + "documentation": "A URI for the location of the folder." + } + ], + "documentation": "The parameters sent in a request to read the contents of a directory.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "name": "DirectoryEntry", + "properties": [ + { + "name": "name", + "type": { + "kind": "base", + "name": "string" + }, + "documentation": "The name of the entry." + }, + { + "name": "type", + "type": { + "kind": "reference", + "name": "FileType" + }, + "documentation": "The type of the entry." + }, + { + "name": "isSymlink", + "type": { + "kind": "base", + "name": "boolean" + }, + "documentation": "Whether the entry is a symbolic link." + } + ], + "documentation": "A directory entry represents a file or a folder in a directory.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "name": "ReadFileParams", + "properties": [ + { + "name": "uri", + "type": { + "kind": "base", + "name": "DocumentUri" + }, + "documentation": "A URI for the location of the file." + }, + { + "name": "encoding", + "type": { + "kind": "base", + "name": "string" + }, + "optional": true, + "documentation": "The encoding of the file content. If not specified, the content is assumed to be UTF-8." + } + ], + "documentation": "The parameters sent in a request to read the contents of a file.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "name": "ReadFileResult", + "properties": [ + { + "name": "text", + "type": { + "kind": "base", + "name": "string" + }, + "documentation": "The content of the file as a unicode string.\nIt will be read using the encoding specified in the request.\nAny invalid byte sequences will be replaced with the unicode replacement character `U+FFFD`." + } + ], + "documentation": "The result of a read file request.\n\n@since 3.19.0", + "since": "3.19.0" + }, { "name": "RegistrationParams", "properties": [ @@ -11237,6 +11462,16 @@ "optional": true, "documentation": "Capabilities specific to the `workspace/textDocumentContent` request.\n\n@since 3.18.0", "since": "3.18.0" + }, + { + "name": "fileSystem", + "type": { + "kind": "reference", + "name": "FileSystemClientCapabilities" + }, + "optional": true, + "documentation": "Client capabilities specific to file system requests.\n\n@since 3.19.0", + "since": "3.19.0" } ], "documentation": "Workspace specific client capabilities." @@ -12301,6 +12536,40 @@ "documentation": "Client capabilities for a text document content provider.\n\n@since 3.18.0", "since": "3.18.0" }, + { + "name": "FileSystemClientCapabilities", + "properties": [ + { + "name": "stat", + "type": { + "kind": "base", + "name": "boolean" + }, + "optional": true, + "documentation": "Whether the client supports the `workspace/stat` request." + }, + { + "name": "readDirectory", + "type": { + "kind": "base", + "name": "boolean" + }, + "optional": true, + "documentation": "Whether the client supports the `workspace/readDirectory` request." + }, + { + "name": "readFile", + "type": { + "kind": "base", + "name": "boolean" + }, + "optional": true, + "documentation": "Whether the client supports the `workspace/readFile` request." + } + ], + "documentation": "Client capabilities specific to file system requests.\n\n@since 3.19.0", + "since": "3.19.0" + }, { "name": "TextDocumentSyncClientCapabilities", "properties": [ @@ -14632,6 +14901,32 @@ "documentation": "Inlay hint kinds.\n\n@since 3.17.0", "since": "3.17.0" }, + { + "name": "FileType", + "type": { + "kind": "base", + "name": "string" + }, + "values": [ + { + "name": "unknown", + "value": "unknown", + "documentation": "The file type is unknown." + }, + { + "name": "file", + "value": "file", + "documentation": "A regular file." + }, + { + "name": "directory", + "value": "directory", + "documentation": "A directory." + } + ], + "documentation": "The file type of a file system entry.\n\n@since 3.19.0", + "since": "3.19.0" + }, { "name": "MessageType", "type": { diff --git a/protocol/src/common/protocol.fileSystem.ts b/protocol/src/common/protocol.fileSystem.ts new file mode 100644 index 000000000..eb64bf85b --- /dev/null +++ b/protocol/src/common/protocol.fileSystem.ts @@ -0,0 +1,202 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { RequestHandler } from 'vscode-jsonrpc'; +import { type DocumentUri } from 'vscode-languageserver-types'; +import { CM, MessageDirection, ProtocolRequestType } from './messages'; + +/** + * Client capabilities specific to file system requests. + * + * @since 3.19.0 + */ +export interface FileSystemClientCapabilities { + + /** + * Whether the client supports the `workspace/stat` request. + */ + stat?: boolean; + + /** + * Whether the client supports the `workspace/readDirectory` request. + */ + readDirectory?: boolean; + + /** + * Whether the client supports the `workspace/readFile` request. + */ + readFile?: boolean; +} + +/** + * Represents metadata about a file. + * + * @since 3.19.0 + */ +export interface FileStat { + /** + * The type of the file, e.g. is a regular file or a directory. + */ + type: FileType; + /** + * Whether the file is a symbolic link. + */ + isSymlink: boolean; + /** + * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + ctime: number; + /** + * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + mtime: number; + /** + * The size in bytes. + */ + size: number; +} + +/** + * The parameters sent in a request to get metadata about a file. + * + * @since 3.19.0 + */ +export interface StatParams { + /** + * A URI for the location of the file/folder. + */ + uri: DocumentUri; +} + +/** + * The file type of a file system entry. + * + * @since 3.19.0 + */ +export namespace FileType { + /** + * The file type is unknown. + */ + export const unknown = 'unknown'; + /** + * A regular file. + */ + export const file = 'file'; + /** + * A directory. + */ + export const directory = 'directory'; +} +export type FileType = 'unknown' | 'file' | 'directory'; + +/** + * The parameters sent in a request to read the contents of a directory. + * + * @since 3.19.0 + */ +export interface ReadDirectoryParams { + /** + * A URI for the location of the folder. + */ + uri: DocumentUri; +} + +/** + * A directory entry represents a file or a folder in a directory. + * + * @since 3.19.0 + */ +export interface DirectoryEntry { + /** + * The name of the entry. + */ + name: string; + /** + * The type of the entry. + */ + type: FileType; + /** + * Whether the entry is a symbolic link. + */ + isSymlink: boolean; +} + +/** + * The parameters sent in a request to read the contents of a file. + * + * @since 3.19.0 + */ +export interface ReadFileParams { + /** + * A URI for the location of the file. + */ + uri: DocumentUri; + /** + * The encoding of the file content. If not specified, the content is assumed to be UTF-8. + */ + encoding?: string; +} + +/** + * The result of a read file request. + * + * @since 3.19.0 + */ +export interface ReadFileResult { + /** + * The content of the file as a unicode string. + * It will be read using the encoding specified in the request. + * Any invalid byte sequences will be replaced with the unicode replacement character `U+FFFD`. + */ + text: string; +} + +/** + * The stat request is sent from the server to the client to get metadata about a file. + * + * The request can return a `FileStat` which will be used to determine the type of the file, + * its size, and the creation and modification time. Returns `null` if the file does not exist. + * + * @since 3.19.0 + */ +export namespace StatRequest { + export const method: 'workspace/stat' = 'workspace/stat'; + export const messageDirection: MessageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); + export type HandlerSignature = RequestHandler; + export const capabilities = CM.create('workspace.fileOperations.fileStat', undefined); +} + +/** + * The read directory request is sent from the server to the client to get the entries of a directory. + * + * The request can return a `DirectoryEntry[]` which contains the directory entries. + * Returns `null` if the directory does not exist or the client cannot read it. + * + * @since 3.19.0 + */ +export namespace ReadDirectoryRequest { + export const method: 'workspace/readDirectory' = 'workspace/readDirectory'; + export const messageDirection: MessageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); + export type HandlerSignature = RequestHandler; + export const capabilities = CM.create('workspace.fileOperations.readDirectory', undefined); +} + +/** + * The read file request is sent from the server to the client to get the content of a file. + * + * The request can return a `ReadFileResult` which contains the content of the file. + * Returns `null` if the file does not exist or the client cannot read it. + * + * @since 3.19.0 + */ +export namespace ReadFileRequest { + export const method: 'workspace/readFile' = 'workspace/readFile'; + export const messageDirection: MessageDirection = MessageDirection.serverToClient; + export const type = new ProtocolRequestType(method); + export type HandlerSignature = RequestHandler; + export const capabilities = CM.create('workspace.fileOperations.readFile', undefined); +} diff --git a/protocol/src/common/protocol.ts b/protocol/src/common/protocol.ts index 01f8b6d0f..badaff639 100644 --- a/protocol/src/common/protocol.ts +++ b/protocol/src/common/protocol.ts @@ -131,6 +131,11 @@ import { TextDocumentContentRequest, TextDocumentContentRefreshParams, TextDocumentContentRefreshRequest } from './protocol.textDocumentContent'; +import { + FileStat, StatParams, StatRequest, DirectoryEntry, FileType, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult, + FileSystemClientCapabilities +} from './protocol.fileSystem'; + // @ts-ignore: to avoid inlining LocationLink as dynamic import let __noDynamicImport: LocationLink | undefined; @@ -674,6 +679,13 @@ export interface WorkspaceClientCapabilities { * @since 3.18.0 */ textDocumentContent?: TextDocumentContentClientCapabilities; + + /** + * Client capabilities specific to file system requests. + * + * @since 3.19.0 + */ + fileSystem?: FileSystemClientCapabilities; } /** @@ -4379,7 +4391,9 @@ export { InlineCompletionClientCapabilities, InlineCompletionOptions, InlineCompletionParams, InlineCompletionRegistrationOptions, InlineCompletionRequest, // Text Document Content TextDocumentContentClientCapabilities, TextDocumentContentOptions, TextDocumentContentRegistrationOptions, TextDocumentContentParams, TextDocumentContentResult, - TextDocumentContentRequest, TextDocumentContentRefreshParams, TextDocumentContentRefreshRequest + TextDocumentContentRequest, TextDocumentContentRefreshParams, TextDocumentContentRefreshRequest, + // File System + FileStat, StatParams, StatRequest, DirectoryEntry, FileType, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult, }; // To be backwards compatible diff --git a/server/src/common/fileSystem.ts b/server/src/common/fileSystem.ts new file mode 100644 index 000000000..a473997ed --- /dev/null +++ b/server/src/common/fileSystem.ts @@ -0,0 +1,60 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { DirectoryEntry, FileStat, StatRequest, ReadDirectoryRequest, ReadFileRequest, ReadFileResult, type DocumentUri } from 'vscode-languageserver-protocol'; + +import type { Feature, _RemoteWorkspace } from './server'; + +/** + * Shape of the file system feature + * + * @since 3.19.0 + */ +export interface FileSystemFeatureShape { + /** + * Provides access to the file system of the client. + * + * @since 3.19.0 + */ + fs: { + /** + * Returns metadata about a file system entry. + * + * @param uri The URI of the file to stat. + */ + stat(uri: DocumentUri): Promise; + /** + * Reads the contents of a file. + * + * @param uri The URI of the file to read. + * @param encoding The encoding to use when reading the file. Uses UTF-8 if not specified. + */ + readFile(uri: DocumentUri, encoding?: string): Promise; + /** + * Reads the contents of a directory. + * + * @param uri The URI of the directory to read. + */ + readDirectory(uri: DocumentUri): Promise; + }; +} + +export const FileSystemFeature: Feature<_RemoteWorkspace, FileSystemFeatureShape> = (Base) => { + return class extends Base { + public get fs() { + return { + stat: (uri: DocumentUri): Promise => { + return this.connection.sendRequest(StatRequest.type, { uri }); + }, + readFile: (uri: DocumentUri, encoding?: string): Promise => { + return this.connection.sendRequest(ReadFileRequest.type, { uri, encoding }); + }, + readDirectory: (uri: DocumentUri): Promise => { + return this.connection.sendRequest(ReadDirectoryRequest.type, { uri }); + } + }; + } + }; +}; diff --git a/server/src/common/server.ts b/server/src/common/server.ts index 9f68da04b..da7779b54 100644 --- a/server/src/common/server.ts +++ b/server/src/common/server.ts @@ -49,6 +49,7 @@ import { MonikerFeature, MonikerFeatureShape } from './moniker'; import type { ConnectionState } from './textDocuments'; import { InlineCompletionFeature, type InlineCompletionFeatureShape } from './inlineCompletion'; import { TextDocumentContentFeature, type TextDocumentContentFeatureShape } from './textDocumentContent'; +import { FileSystemFeature, FileSystemFeatureShape } from './fileSystem'; function null2Undefined(value: T | null): T | undefined { if (value === null) { @@ -646,7 +647,7 @@ export interface _RemoteWorkspace extends FeatureBase { applyEdit(paramOrEdit: ApplyWorkspaceEditParams | WorkspaceEdit): Promise; } -export type RemoteWorkspace = _RemoteWorkspace & Configuration & WorkspaceFolders & FileOperationsFeatureShape & TextDocumentContentFeatureShape; +export type RemoteWorkspace = _RemoteWorkspace & Configuration & WorkspaceFolders & FileOperationsFeatureShape & TextDocumentContentFeatureShape & FileSystemFeatureShape; class _RemoteWorkspaceImpl implements _RemoteWorkspace, Remote { @@ -682,7 +683,7 @@ class _RemoteWorkspaceImpl implements _RemoteWorkspace, Remote { } } -const RemoteWorkspaceImpl: new () => RemoteWorkspace = TextDocumentContentFeature(FileOperationsFeature(WorkspaceFoldersFeature(ConfigurationFeature(_RemoteWorkspaceImpl)))) as (new () => RemoteWorkspace); +const RemoteWorkspaceImpl: new () => RemoteWorkspace = FileSystemFeature(TextDocumentContentFeature(FileOperationsFeature(WorkspaceFoldersFeature(ConfigurationFeature(_RemoteWorkspaceImpl))))) as (new () => RemoteWorkspace); /** * Interface to log telemetry events. The events are actually send to the client diff --git a/testbed/client/src/extension.ts b/testbed/client/src/extension.ts index 89575fd5e..c16711ebd 100644 --- a/testbed/client/src/extension.ts +++ b/testbed/client/src/extension.ts @@ -116,6 +116,32 @@ REM or .bmp extension from c:\\source to c:\\images;;`; commands.registerCommand('testbed.refreshContent', async () => { await client.sendNotification(refreshNotification, 'test-content://file.txt'); }); + + const readFileRequest = new NotificationType('testbed/readFile'); + commands.registerCommand('testbed.readCurrentFile', async () => { + const uri = window.activeTextEditor?.document.uri.toString(); + if (uri) { + await client.sendNotification(readFileRequest, uri); + } + }); + + const readDirectoryRequest = new NotificationType('testbed/readDirectory'); + commands.registerCommand('testbed.readCurrentDirectory', async () => { + const activeEditorUri = window.activeTextEditor?.document.uri; + if (!activeEditorUri) { + return; + } + const directoryUri = activeEditorUri.with({ path: path.dirname(activeEditorUri.path) }); + await client.sendNotification(readDirectoryRequest, directoryUri.toString()); + }); + + const statRequest = new NotificationType('testbed/stat'); + commands.registerCommand('testbed.statCurrentFile', async () => { + const uri = window.activeTextEditor?.document.uri.toString(); + if (uri) { + await client.sendNotification(statRequest, uri); + } + }); } export function deactivate() { diff --git a/testbed/package.json b/testbed/package.json index b167a9312..ddfc3359e 100644 --- a/testbed/package.json +++ b/testbed/package.json @@ -25,6 +25,18 @@ { "command": "testbed.refreshContent", "title": "Refresh dynamic content" + }, + { + "command": "testbed.readCurrentFile", + "title": "Read Current File" + }, + { + "command": "testbed.readCurrentDirectory", + "title": "Read Current Directory" + }, + { + "command": "testbed.statCurrentFile", + "title": "Stat Current File" } ], "configuration": { diff --git a/testbed/server/src/server.ts b/testbed/server/src/server.ts index 95110dc67..c55858393 100644 --- a/testbed/server/src/server.ts +++ b/testbed/server/src/server.ts @@ -18,7 +18,7 @@ import { SemanticTokensClientCapabilities, SemanticTokensLegend, SemanticTokensBuilder, SemanticTokensRegistrationType, SemanticTokensRegistrationOptions, ProtocolNotificationType, ChangeAnnotation, WorkspaceChange, CompletionItemKind, DiagnosticSeverity, DocumentDiagnosticReportKind, WorkspaceDiagnosticReport, NotebookDocuments, CompletionList, DidChangeConfigurationNotification, - NotificationType + NotificationType, FileType } from 'vscode-languageserver/node'; import { @@ -717,6 +717,42 @@ connection.onNotification(refreshNotification, async (uri) => { await connection.workspace.textDocumentContent.refresh(uri); }); +const readFileRequest = new NotificationType('testbed/readFile'); +connection.onNotification(readFileRequest, async (uri) => { + const fileName = uri.split('/').pop(); + const fileContent = await connection.workspace.fs.readFile(uri); + if (fileContent === null) { + connection.window.showInformationMessage(`Read file '${fileName}' failed`); + } else { + connection.window.showInformationMessage(`Read file '${fileName}' with content length ${fileContent.text.length}`); + } +}); + +const readDirectoryRequest = new NotificationType('testbed/readDirectory'); +connection.onNotification(readDirectoryRequest, async (uri) => { + const dirName = uri.split('/').pop(); + const directoryContent = await connection.workspace.fs.readDirectory(uri); + if (directoryContent === null) { + connection.window.showInformationMessage(`Read directory '${dirName}' failed`); + } else { + connection.window.showInformationMessage(`Read directory '${dirName}' with ${directoryContent.length} entries`); + } +}); + +const statRequest = new NotificationType('testbed/stat'); +connection.onNotification(statRequest, async (uri) => { + const fileName = uri.split('/').pop(); + const fileStat = await connection.workspace.fs.stat(uri); + if (fileStat === null) { + connection.window.showInformationMessage(`File stat ${fileName} failed`); + } else { + let type = fileStat.type; + if (fileStat.isSymlink) { + type += ' (symlink)'; + } + connection.window.showInformationMessage(`File stat '${fileName}' with type '${type}' and size ${fileStat.size}`); + } +}); const notebooks = new NotebookDocuments(TextDocument); notebooks.onDidOpen(() => { From b356b60cc8e00bb6674be8172e4b6e5029db0285 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Mon, 10 Aug 2026 16:08:25 +0200 Subject: [PATCH 2/3] Make isSymlink a flag property --- client/src/common/codeConverter.ts | 17 +++++++----- protocol/metaModel.json | 32 ++++++++++++++++------ protocol/src/common/protocol.fileSystem.ts | 22 +++++++++++---- protocol/src/common/protocol.ts | 4 +-- testbed/server/src/server.ts | 4 +-- 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/client/src/common/codeConverter.ts b/client/src/common/codeConverter.ts index da1cdd53c..cca0a9985 100644 --- a/client/src/common/codeConverter.ts +++ b/client/src/common/codeConverter.ts @@ -982,7 +982,7 @@ export function createConverter(uriConverter?: URIConverter): Converter { function asFileType(value: code.FileType): { type: proto.FileType; - isSymlink: boolean; + flags: proto.FileFlags; } { let type: proto.FileType = proto.FileType.unknown; if (value & code.FileType.Directory) { @@ -990,28 +990,31 @@ export function createConverter(uriConverter?: URIConverter): Converter { } else if (value & code.FileType.File) { type = proto.FileType.file; } - const isSymlink = (value & code.FileType.SymbolicLink) !== 0; + let flags: proto.FileFlags = 0; + if (value & code.FileType.SymbolicLink) { + flags |= proto.FileFlags.symbolicLink; + } return { type, - isSymlink + flags }; } function asDirectoryEntry(item: [string, code.FileType]): proto.DirectoryEntry { - const { type, isSymlink } = asFileType(item[1]); + const { type, flags } = asFileType(item[1]); const result: proto.DirectoryEntry = { name: item[0], type, - isSymlink + flags }; return result; } function asFileStat(item: code.FileStat): proto.FileStat { - const { type, isSymlink } = asFileType(item.type); + const { type, flags } = asFileType(item.type); const result: proto.FileStat = { type, - isSymlink, + flags, ctime: item.ctime, mtime: item.mtime, size: item.size diff --git a/protocol/metaModel.json b/protocol/metaModel.json index d6f36d984..92e403e88 100644 --- a/protocol/metaModel.json +++ b/protocol/metaModel.json @@ -4622,12 +4622,12 @@ "documentation": "The type of the file, e.g. is a regular file or a directory." }, { - "name": "isSymlink", + "name": "flags", "type": { - "kind": "base", - "name": "boolean" + "kind": "reference", + "name": "FileFlags" }, - "documentation": "Whether the file is a symbolic link." + "documentation": "Additional flags about the file." }, { "name": "ctime", @@ -4692,12 +4692,12 @@ "documentation": "The type of the entry." }, { - "name": "isSymlink", + "name": "flags", "type": { - "kind": "base", - "name": "boolean" + "kind": "reference", + "name": "FileFlags" }, - "documentation": "Whether the entry is a symbolic link." + "documentation": "Additional flags about the entry." } ], "documentation": "A directory entry represents a file or a folder in a directory.\n\n@since 3.19.0", @@ -14927,6 +14927,22 @@ "documentation": "The file type of a file system entry.\n\n@since 3.19.0", "since": "3.19.0" }, + { + "name": "FileFlags", + "type": { + "kind": "base", + "name": "uinteger" + }, + "values": [ + { + "name": "symbolicLink", + "value": 1, + "documentation": "The file is a symbolic link." + } + ], + "supportsCustomValues": true, + "documentation": "Additional flags about a file system entry.\nImplemented as a bitmask so that multiple flags can be combined." + }, { "name": "MessageType", "type": { diff --git a/protocol/src/common/protocol.fileSystem.ts b/protocol/src/common/protocol.fileSystem.ts index eb64bf85b..3c9676d29 100644 --- a/protocol/src/common/protocol.fileSystem.ts +++ b/protocol/src/common/protocol.fileSystem.ts @@ -4,7 +4,7 @@ * ------------------------------------------------------------------------------------------ */ import { RequestHandler } from 'vscode-jsonrpc'; -import { type DocumentUri } from 'vscode-languageserver-types'; +import { uinteger, type DocumentUri } from 'vscode-languageserver-types'; import { CM, MessageDirection, ProtocolRequestType } from './messages'; /** @@ -41,9 +41,9 @@ export interface FileStat { */ type: FileType; /** - * Whether the file is a symbolic link. + * Additional flags about the file. */ - isSymlink: boolean; + flags: FileFlags; /** * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. */ @@ -91,6 +91,18 @@ export namespace FileType { } export type FileType = 'unknown' | 'file' | 'directory'; +/** + * Additional flags about a file system entry. + * Implemented as a bitmask so that multiple flags can be combined. + */ +export namespace FileFlags { + /** + * The file is a symbolic link. + */ + export const symbolicLink = 1; +} +export type FileFlags = uinteger; + /** * The parameters sent in a request to read the contents of a directory. * @@ -118,9 +130,9 @@ export interface DirectoryEntry { */ type: FileType; /** - * Whether the entry is a symbolic link. + * Additional flags about the entry. */ - isSymlink: boolean; + flags: FileFlags; } /** diff --git a/protocol/src/common/protocol.ts b/protocol/src/common/protocol.ts index badaff639..d13b6ad4e 100644 --- a/protocol/src/common/protocol.ts +++ b/protocol/src/common/protocol.ts @@ -133,7 +133,7 @@ import { import { FileStat, StatParams, StatRequest, DirectoryEntry, FileType, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult, - FileSystemClientCapabilities + FileSystemClientCapabilities, FileFlags } from './protocol.fileSystem'; // @ts-ignore: to avoid inlining LocationLink as dynamic import @@ -4393,7 +4393,7 @@ export { TextDocumentContentClientCapabilities, TextDocumentContentOptions, TextDocumentContentRegistrationOptions, TextDocumentContentParams, TextDocumentContentResult, TextDocumentContentRequest, TextDocumentContentRefreshParams, TextDocumentContentRefreshRequest, // File System - FileStat, StatParams, StatRequest, DirectoryEntry, FileType, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult, + FileStat, StatParams, StatRequest, DirectoryEntry, FileType, FileFlags, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult, }; // To be backwards compatible diff --git a/testbed/server/src/server.ts b/testbed/server/src/server.ts index c55858393..22e898be3 100644 --- a/testbed/server/src/server.ts +++ b/testbed/server/src/server.ts @@ -18,7 +18,7 @@ import { SemanticTokensClientCapabilities, SemanticTokensLegend, SemanticTokensBuilder, SemanticTokensRegistrationType, SemanticTokensRegistrationOptions, ProtocolNotificationType, ChangeAnnotation, WorkspaceChange, CompletionItemKind, DiagnosticSeverity, DocumentDiagnosticReportKind, WorkspaceDiagnosticReport, NotebookDocuments, CompletionList, DidChangeConfigurationNotification, - NotificationType, FileType + NotificationType, FileFlags } from 'vscode-languageserver/node'; import { @@ -747,7 +747,7 @@ connection.onNotification(statRequest, async (uri) => { connection.window.showInformationMessage(`File stat ${fileName} failed`); } else { let type = fileStat.type; - if (fileStat.isSymlink) { + if (fileStat.flags & FileFlags.symbolicLink) { type += ' (symlink)'; } connection.window.showInformationMessage(`File stat '${fileName}' with type '${type}' and size ${fileStat.size}`); From 2b1c33e41549ffe469aa5b1f004c28ccbe5ca543 Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Fri, 28 Aug 2026 10:42:28 +0200 Subject: [PATCH 3/3] Support binary file data --- client/src/common/fileSystem.ts | 56 ++++++++++-- protocol/metaModel.json | 99 ++++++++++++++++------ protocol/src/common/protocol.fileSystem.ts | 42 ++++++++- protocol/src/common/protocol.ts | 3 +- server/src/common/fileSystem.ts | 16 ++-- testbed/server/src/server.ts | 8 +- 6 files changed, 176 insertions(+), 48 deletions(-) diff --git a/client/src/common/fileSystem.ts b/client/src/common/fileSystem.ts index 60c132f75..0b547da1d 100644 --- a/client/src/common/fileSystem.ts +++ b/client/src/common/fileSystem.ts @@ -6,13 +6,14 @@ import * as vscode from 'vscode'; import { - ClientCapabilities, StatRequest, ReadFileRequest, ReadDirectoryRequest + ClientCapabilities, StatRequest, ReadFileRequest, ReadDirectoryRequest, + ReadFileParamKind } from 'vscode-languageserver-protocol'; import { StaticFeature, FeatureClient, FeatureState } from './features'; export interface FileSystemReadFileSignature { - (this: void, uri: vscode.Uri, encoding?: string): Promise; + (this: void, kind: ReadFileParamKind, uri: vscode.Uri, encoding?: string): Promise; } export interface FileSystemStatSignature { @@ -31,7 +32,7 @@ export interface FileSystemReadDirectorySignature { export interface FileSystemMiddleware { fs?: { stat?: (this: void, uri: vscode.Uri, next: FileSystemStatSignature) => vscode.ProviderResult; - readFile?: (this: void, uri: vscode.Uri, encoding: string | undefined, next: FileSystemReadFileSignature) => vscode.ProviderResult; + readFile?: (this: void, kind: ReadFileParamKind, uri: vscode.Uri, encoding: string | undefined, next: FileSystemReadFileSignature) => vscode.ProviderResult; readDirectory?: (this: void, uri: vscode.Uri, next: FileSystemReadDirectorySignature) => vscode.ProviderResult<[string, vscode.FileType][] | null>; }; } @@ -92,24 +93,31 @@ export class FileSystemFeature implements StaticFeature { : null; }); client.onRequest(ReadFileRequest.type, async (params) => { + const encoding = params.kind === 'text' ? params.encoding : undefined; const paramsUri = this._client.protocol2CodeConverter.asUri(params.uri); - const fileRead: FileSystemReadFileSignature = async (uri, encoding) => { + const fileRead: FileSystemReadFileSignature = async (kind, uri, encoding) => { try { const bytes = await vscode.workspace.fs.readFile(uri); - const decoder = new TextDecoder(encoding || 'utf-8'); - return decoder.decode(bytes); + if (kind === 'binary') { + return toBase64(bytes); + } else if (kind === 'text') { + const decoder = new TextDecoder(encoding || 'utf-8'); + return decoder.decode(bytes); + } else { + return null; + } } catch { return null; } }; const middleware = client.middleware.workspace; const result = await (middleware?.fs?.readFile - ? middleware.fs.readFile(paramsUri, params.encoding, fileRead) - : fileRead(paramsUri, params.encoding)); + ? middleware.fs.readFile(params.kind, paramsUri, encoding, fileRead) + : fileRead(params.kind, paramsUri, encoding)); if (result === undefined || result === null) { return null; } - return { text: result }; + return { content: result }; }); client.onRequest(ReadDirectoryRequest.type, async (params) => { const paramsUri = this._client.protocol2CodeConverter.asUri(params.uri); @@ -136,3 +144,33 @@ export class FileSystemFeature implements StaticFeature { public clear(): void { } } + +declare const Buffer: undefined | { + from(bytes: Uint8Array): { toString(encoding: 'base64'): string }; +}; + +const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +function toBase64(bytes: Uint8Array): string { + // First, try to use the new toBase64() method on Uint8Array if available. + if (typeof (bytes as any).toBase64 === 'function') { + return (bytes as any).toBase64(); + } + // Then, try to use the node.js buffer implementation if available. + if (typeof Buffer !== 'undefined') { + return Buffer.from(bytes).toString('base64'); + } + // Fallback to a manual implementation, which is slower but works in all environments. + // File might be quite large, use an array to avoid concat overhead + const chars = new Array(Math.ceil(bytes.length / 3) * 4); + let j = 0; + for (let i = 0; i < bytes.length; i += 3) { + const b0 = bytes[i]; + const b1 = i + 1 < bytes.length ? bytes[i + 1] : 0; + const b2 = i + 2 < bytes.length ? bytes[i + 2] : 0; + chars[j++] = base64Chars[b0 >> 2]; + chars[j++] = base64Chars[((b0 & 0x03) << 4) | (b1 >> 4)]; + chars[j++] = i + 1 < bytes.length ? base64Chars[((b1 & 0x0f) << 2) | (b2 >> 6)] : '='; + chars[j++] = i + 2 < bytes.length ? base64Chars[b2 & 0x3f] : '='; + } + return chars.join(''); +} diff --git a/protocol/metaModel.json b/protocol/metaModel.json index 92e403e88..8f79bd45c 100644 --- a/protocol/metaModel.json +++ b/protocol/metaModel.json @@ -4703,40 +4703,16 @@ "documentation": "A directory entry represents a file or a folder in a directory.\n\n@since 3.19.0", "since": "3.19.0" }, - { - "name": "ReadFileParams", - "properties": [ - { - "name": "uri", - "type": { - "kind": "base", - "name": "DocumentUri" - }, - "documentation": "A URI for the location of the file." - }, - { - "name": "encoding", - "type": { - "kind": "base", - "name": "string" - }, - "optional": true, - "documentation": "The encoding of the file content. If not specified, the content is assumed to be UTF-8." - } - ], - "documentation": "The parameters sent in a request to read the contents of a file.\n\n@since 3.19.0", - "since": "3.19.0" - }, { "name": "ReadFileResult", "properties": [ { - "name": "text", + "name": "content", "type": { "kind": "base", "name": "string" }, - "documentation": "The content of the file as a unicode string.\nIt will be read using the encoding specified in the request.\nAny invalid byte sequences will be replaced with the unicode replacement character `U+FFFD`." + "documentation": "The content of the file as a unicode string.\n\nIf the content is requested as text, it will be read using the encoding specified in the request.\nAny invalid byte sequences will be replaced with the unicode replacement character `U+FFFD`.\n\nIf the content is requested as binary, it will be returned as a base64 encoded string." } ], "documentation": "The result of a read file request.\n\n@since 3.19.0", @@ -8273,6 +8249,61 @@ "documentation": "Text document content provider options.\n\n@since 3.18.0", "since": "3.18.0" }, + { + "name": "TextReadFileParams", + "properties": [ + { + "name": "kind", + "type": { + "kind": "stringLiteral", + "value": "text" + }, + "documentation": "Indicator that the file content should be read as a text file." + }, + { + "name": "uri", + "type": { + "kind": "base", + "name": "DocumentUri" + }, + "documentation": "A URI for the location of the file." + }, + { + "name": "encoding", + "type": { + "kind": "base", + "name": "string" + }, + "optional": true, + "documentation": "The encoding of the file content. If not specified, the content is assumed to be UTF-8." + } + ], + "documentation": "The parameters sent in a request to read the text contents of a file.\nFile will be read using the encoding specified in the request.\n\n@since 3.19.0", + "since": "3.19.0" + }, + { + "name": "BinaryReadFileParams", + "properties": [ + { + "name": "kind", + "type": { + "kind": "stringLiteral", + "value": "binary" + }, + "documentation": "Indicator that the file content should be read as a binary file." + }, + { + "name": "uri", + "type": { + "kind": "base", + "name": "DocumentUri" + }, + "documentation": "A URI for the location of the file." + } + ], + "documentation": "The parameters sent in a request to read the binary contents of a file.\nFile content will be returned as a base64 encoded string.\n\n@since 3.19.0", + "since": "3.19.0" + }, { "name": "Registration", "properties": [ @@ -16150,6 +16181,22 @@ "documentation": "The document diagnostic report used when reporting partial result.\n\nWhen using partial results, the first literal sent needs to be a\nDocumentDiagnosticReport providing the diagnostics on the document\nfollowed by n DocumentDiagnosticReportPartialResult literals providing\nthe diagnostics for related documents.\n\n```\nDocumentDiagnosticReport\nDocumentDiagnosticReportPartialResult\nDocumentDiagnosticReportPartialResult\n...\n```\n\n@since 3.18.1", "since": "3.18.1" }, + { + "name": "ReadFileParams", + "type": { + "kind": "or", + "items": [ + { + "kind": "reference", + "name": "TextReadFileParams" + }, + { + "kind": "reference", + "name": "BinaryReadFileParams" + } + ] + } + }, { "name": "PrepareRenameResult", "type": { diff --git a/protocol/src/common/protocol.fileSystem.ts b/protocol/src/common/protocol.fileSystem.ts index 3c9676d29..3e01cf6ec 100644 --- a/protocol/src/common/protocol.fileSystem.ts +++ b/protocol/src/common/protocol.fileSystem.ts @@ -136,11 +136,16 @@ export interface DirectoryEntry { } /** - * The parameters sent in a request to read the contents of a file. + * The parameters sent in a request to read the text contents of a file. + * File will be read using the encoding specified in the request. * * @since 3.19.0 */ -export interface ReadFileParams { +export interface TextReadFileParams { + /** + * Indicator that the file content should be read as a text file. + */ + kind: 'text'; /** * A URI for the location of the file. */ @@ -151,6 +156,32 @@ export interface ReadFileParams { encoding?: string; } +/** + * The parameters sent in a request to read the binary contents of a file. + * File content will be returned as a base64 encoded string. + * + * @since 3.19.0 + */ +export interface BinaryReadFileParams { + /** + * Indicator that the file content should be read as a binary file. + */ + kind: 'binary'; + /** + * A URI for the location of the file. + */ + uri: DocumentUri; +} + +/** + * The parameters sent in a request to read the contents of a file. + * + * @since 3.19.0 + */ +export type ReadFileParams = TextReadFileParams | BinaryReadFileParams; +export type ReadFileParamKind = ReadFileParams['kind']; + + /** * The result of a read file request. * @@ -159,10 +190,13 @@ export interface ReadFileParams { export interface ReadFileResult { /** * The content of the file as a unicode string. - * It will be read using the encoding specified in the request. + * + * If the content is requested as text, it will be read using the encoding specified in the request. * Any invalid byte sequences will be replaced with the unicode replacement character `U+FFFD`. + * + * If the content is requested as binary, it will be returned as a base64 encoded string. */ - text: string; + content: string; } /** diff --git a/protocol/src/common/protocol.ts b/protocol/src/common/protocol.ts index d13b6ad4e..4fde8d100 100644 --- a/protocol/src/common/protocol.ts +++ b/protocol/src/common/protocol.ts @@ -133,7 +133,7 @@ import { import { FileStat, StatParams, StatRequest, DirectoryEntry, FileType, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult, - FileSystemClientCapabilities, FileFlags + FileSystemClientCapabilities, FileFlags, ReadFileParamKind, TextReadFileParams, BinaryReadFileParams, } from './protocol.fileSystem'; // @ts-ignore: to avoid inlining LocationLink as dynamic import @@ -4394,6 +4394,7 @@ export { TextDocumentContentRequest, TextDocumentContentRefreshParams, TextDocumentContentRefreshRequest, // File System FileStat, StatParams, StatRequest, DirectoryEntry, FileType, FileFlags, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult, + ReadFileParamKind, TextReadFileParams, BinaryReadFileParams, }; // To be backwards compatible diff --git a/server/src/common/fileSystem.ts b/server/src/common/fileSystem.ts index a473997ed..f075de362 100644 --- a/server/src/common/fileSystem.ts +++ b/server/src/common/fileSystem.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import { DirectoryEntry, FileStat, StatRequest, ReadDirectoryRequest, ReadFileRequest, ReadFileResult, type DocumentUri } from 'vscode-languageserver-protocol'; +import { DirectoryEntry, FileStat, StatRequest, ReadDirectoryRequest, ReadFileRequest, ReadFileResult, TextReadFileParams, BinaryReadFileParams, ReadFileParamKind, DocumentUri } from 'vscode-languageserver-protocol'; import type { Feature, _RemoteWorkspace } from './server'; @@ -26,12 +26,18 @@ export interface FileSystemFeatureShape { */ stat(uri: DocumentUri): Promise; /** - * Reads the contents of a file. + * Reads the contents of a file as binary. Content is returned as a base64 encoded string. + * + * @param uri The URI of the file to read. + */ + readFile(kind: BinaryReadFileParams['kind'], uri: DocumentUri): Promise; + /** + * Reads the contents of a file as text. * * @param uri The URI of the file to read. * @param encoding The encoding to use when reading the file. Uses UTF-8 if not specified. */ - readFile(uri: DocumentUri, encoding?: string): Promise; + readFile(kind: TextReadFileParams['kind'], uri: DocumentUri, encoding?: string): Promise; /** * Reads the contents of a directory. * @@ -48,8 +54,8 @@ export const FileSystemFeature: Feature<_RemoteWorkspace, FileSystemFeatureShape stat: (uri: DocumentUri): Promise => { return this.connection.sendRequest(StatRequest.type, { uri }); }, - readFile: (uri: DocumentUri, encoding?: string): Promise => { - return this.connection.sendRequest(ReadFileRequest.type, { uri, encoding }); + readFile: (kind: ReadFileParamKind, uri: DocumentUri, encoding?: string): Promise => { + return this.connection.sendRequest(ReadFileRequest.type, { kind, uri, encoding }); }, readDirectory: (uri: DocumentUri): Promise => { return this.connection.sendRequest(ReadDirectoryRequest.type, { uri }); diff --git a/testbed/server/src/server.ts b/testbed/server/src/server.ts index 22e898be3..b1a4b5aef 100644 --- a/testbed/server/src/server.ts +++ b/testbed/server/src/server.ts @@ -720,11 +720,13 @@ connection.onNotification(refreshNotification, async (uri) => { const readFileRequest = new NotificationType('testbed/readFile'); connection.onNotification(readFileRequest, async (uri) => { const fileName = uri.split('/').pop(); - const fileContent = await connection.workspace.fs.readFile(uri); - if (fileContent === null) { + const textContent = await connection.workspace.fs.readFile('text', uri); + const binaryContent = await connection.workspace.fs.readFile('binary', uri); + if (textContent === null || binaryContent === null) { connection.window.showInformationMessage(`Read file '${fileName}' failed`); } else { - connection.window.showInformationMessage(`Read file '${fileName}' with content length ${fileContent.text.length}`); + const base64Content = binaryContent.content.length > 100 ? binaryContent.content.substring(0, 97) + '...' : binaryContent.content; + connection.window.showInformationMessage(`Read file '${fileName}' with content length ${textContent.content.length} and base64 content ${base64Content}`); } });