From d0e4f81b103f601a0f04f2179ec5edcaa9008a5b Mon Sep 17 00:00:00 2001 From: Mark Sujew Date: Tue, 21 Jul 2026 22:15:26 +0200 Subject: [PATCH] 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(() => {