Skip to content

Add support for workspace read requests - #1825

Open
Mark Sujew (msujew) wants to merge 4 commits into
microsoft:mainfrom
msujew:workspace-read-feature
Open

Add support for workspace read requests#1825
Mark Sujew (msujew) wants to merge 4 commits into
microsoft:mainfrom
msujew:workspace-read-feature

Conversation

@msujew

@msujew Mark Sujew (msujew) commented Jul 22, 2026

Copy link
Copy Markdown

Related to microsoft/language-server-protocol#1264 (does not fully resolve it, since this PR does not include features to write into a file system - only read from it).

Adds support for the server to read files/directories/stat info from the client.

As indicated by microsoft/language-server-protocol#1264 (comment), I also thought it'd be best to start with read-only access to the file system. However, the new interfaces/types should be extendable enough to also add write requests if required later on.

Some questions/considerations:

  • Right now, if the read/stat fails, the LSP returns null. Should it return a response error instead?
  • I'm not entirely sure about the properties in FileSystemClientCapabilities. Does it make sense to expose each individual request type as an opt-in flag? Or is it enough to provide a read?: boolean flag (maybe extend this with a write?: boolean flag later on)?
  • The FileType.unknown value results from the fact that vscode offers vscode.FileType.unknown as a possible value to return for FileStat.type. Should this be included in the protocol, or should stats/directory entries with this type simply be omitted?
  • The LSP has been using folder instead of directory for the most part. Should this change align to this? I've simply used the same nomenclature as vscode does.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@msujew

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@dbaeumer Dirk Bäumer (dbaeumer) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks. Very nice PR.

/**
* Whether the file is a symbolic link.
*/
isSymlink: boolean;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we make this a flag property with a bit wise implementation. Makes it easier to expand in the future.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I thought about combining it with the type property, similar to how its done in vscode, but decided against it. Is this what you had in mind?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Mark Sujew (@msujew) no, I like that they are separate but I would rename isSymlink to flags and have a SymLink flag. If we have more flags in the future it is easier to extend.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ok, I believe that's what I've done in b356b60 already 👍

/**
* Whether the entry is a symbolic link.
*/
isSymlink: boolean;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See above

@Bakker-Martijn

Martijn Bakker (Bakker-Martijn) commented Aug 19, 2026

Copy link
Copy Markdown

What if we read a file that is binary? I have some library files and we need to obfuscate the source for end-users.
Because of the way the fileread is implemented it is always decoded using textdecoder.

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;
	}
};

Does it make sense when encoding === 'buffer' we return the bytes? Or some other mechanism for this use-case?

I currently am already testing with this pull request and all works fine. But I have used middleware to overwrite the implementation. (Which is also fine for me)

@msujew

Mark Sujew (msujew) commented Aug 19, 2026

Copy link
Copy Markdown
Author

Does it make sense when encoding === 'buffer' we return the bytes? Or some other mechanism for this use-case?

Martijn Bakker (@Bakker-Martijn) WDYT about encoding: "base64" for this and then get the buffer as a base64 string? Otherwise we would need to type the result value as string | number[], with the number[] taking 3-4 bytes per byte in the original file, which we would need to pipe through the jsonrpc protocol (a >200% increase). base64 only increases the size by 33%. I'd be open to integrate this into the PR.

Dirk Bäumer (@dbaeumer) do you have an opinion on this?

@Bakker-Martijn

Martijn Bakker (Bakker-Martijn) commented Aug 19, 2026

Copy link
Copy Markdown

Mark Sujew (@msujew) I had the same thought process. But this is not allowed: https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings

I believe the textEncoder only wants to convert to plain text (not base64 or similar). So this would not resolve my current issue.

I also notices the size increase. Which is not ideal...
I could also handle the decoding in the middleware. This way I can still send plain text through the pipe. However, End-users could see the text when looking at the verbose log of the language server. Not sure if I think this is an real issue though.

Just wanted to point this out. It also depends if binary files will be read by other languages servers or not (perhaps I am one of the few). Can imagine not implementing this, having the middleware as fallback is also OK for me.

--Edit:
Or do you mean skipping the textEncoder when encoding === "base64" and converting the uint8array to a base64 string?
That is also completely fine. I would say that this would indeed make more sense than sending number[] (looking at the size increase).

@msujew

Copy link
Copy Markdown
Author

Or do you mean skipping the textEncoder when encoding === "base64" and converting the uint8array to a base64 string?

Yes, exactly. Essentially just special casing the client code and documenting this into the protocol. I.e. servers can request binary file content via encoding: "base64", which clients should respect.

@Bakker-Martijn

Copy link
Copy Markdown

That would be perfect for my use-case. I think that is an excellent idea :)

@dbaeumer

Copy link
Copy Markdown
Member

I think returning base64 for binary files is the right way to go. However overloading the encoding property for it feels strange since it normally talks about the encoding of the file on disk not of the encoding of the resulting string. So I would rather do something like this:

export enum ReadFileParamKind {
   Text = 'text,
   Binary = 'binary'
}

export interface TextReadFileParams {

 	kind: ReadFileParamKind.Text;

	/**
	 * 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;
}

export interface BinaryReadFileParams {
 	kind: ReadFileParamKind.Binary;

	/**
	 * A URI for the location of the file.
	 */
	uri: DocumentUri;
}

export type ReadFileParams = TextReadFileParams | BinaryReadFileParams;

@msujew

Copy link
Copy Markdown
Author

So I would rather do something like this:

Yep, much better. I've replaced the enum with string literal types, since that works better with the meta model generator script. Since there was no base64 conversion yet, I've tried my best with what was available. That should probably do.

@dbaeumer

Copy link
Copy Markdown
Member

Mark Sujew (@msujew) the @since 3.19.0 should also have a @proposed tag since we usually give the community time for additional feedback. Would you also update the specification here: https://github.com/microsoft/language-server-protocol Then I create a 3.19 version as well.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Filesystem access lacks a security boundary, and capability paths and numeric protocol types are currently inconsistent.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Implements the read-only portion of language-server-protocol#1264, allowing servers to access client-side filesystem data.

Changes:

  • Adds stat, file-read, and directory-read protocol requests.
  • Implements client/server feature plumbing and conversion.
  • Adds testbed commands demonstrating the requests.
File summaries
File Description
client/src/common/client.ts Registers filesystem support and middleware.
client/src/common/codeConverter.ts Converts VS Code filesystem metadata.
client/src/common/fileSystem.ts Handles filesystem requests client-side.
protocol/metaModel.json Models the new protocol API.
protocol/src/common/protocol.fileSystem.ts Defines filesystem requests and types.
protocol/src/common/protocol.ts Exposes filesystem capabilities and types.
server/src/common/fileSystem.ts Adds server-side request methods.
server/src/common/server.ts Integrates filesystem methods into workspaces.
testbed/client/src/extension.ts Adds demonstration commands.
testbed/package.json Contributes testbed command metadata.
testbed/server/src/server.ts Exercises filesystem requests.
Review details

Suppressed comments (5)

protocol/metaModel.json:1197

  • The declared capability is workspace.fileSystem.readDirectory, not workspace.fileOperations.readDirectory. Correcting this prevents generated consumers from checking an unadvertised field.
			"clientCapability": "workspace.fileOperations.readDirectory",

protocol/metaModel.json:1222

  • The declared capability is workspace.fileSystem.readFile, not workspace.fileOperations.readFile. Leaving this path unchanged makes generated capability detection disagree with the client implementation.
			"clientCapability": "workspace.fileOperations.readFile",

protocol/src/common/protocol.fileSystem.ts:58

  • Using number here produces an LSP integer in the metamodel, limiting file sizes to 2^31−1 bytes. Files larger than 2 GiB cannot be represented by the declared protocol type; use an unrestricted numeric base type such as decimal and regenerate the metamodel.
	size: number;

protocol/metaModel.json:4656

  • Normal epoch-millisecond mtime values exceed the LSP integer range. This field needs the unrestricted decimal base type instead.
						"name": "integer"

protocol/metaModel.json:4664

  • An LSP integer tops out at 2^31−1, so this model cannot describe files larger than 2 GiB even though VS Code's FileStat.size can. Use the decimal base type to carry the full numeric value.
						"name": "integer"
  • Files reviewed: 11/11 changed files
  • Comments generated: 10
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

const paramsUri = this._client.protocol2CodeConverter.asUri(params.uri);
const fileRead: FileSystemReadFileSignature = async (kind, uri, encoding) => {
try {
const bytes = await vscode.workspace.fs.readFile(uri);
FoldingRangeProviderMiddleware & DeclarationMiddleware & SelectionRangeProviderMiddleware & CallHierarchyMiddleware & SemanticTokensMiddleware &
LinkedEditingRangeMiddleware & TypeHierarchyMiddleware & InlineValueMiddleware & InlayHintsMiddleware & NotebookDocumentMiddleware & DiagnosticProviderMiddleware &
InlineCompletionMiddleware & TextDocumentContentMiddleware & GeneralMiddleware;
InlineCompletionMiddleware & TextDocumentContentMiddleware & FileSystemMiddleware & GeneralMiddleware;
Comment thread protocol/metaModel.json
]
},
"messageDirection": "serverToClient",
"clientCapability": "workspace.fileOperations.fileStat",
Comment thread protocol/metaModel.json
"name": "ctime",
"type": {
"kind": "base",
"name": "integer"
Comment on lines +50 to +54
ctime: number;
/**
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
mtime: number;
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
export const type = new ProtocolRequestType<StatParams, FileStat | null, never, void, void>(method);
export type HandlerSignature = RequestHandler<StatParams, FileStat | null, void>;
export const capabilities = CM.create('workspace.fileOperations.fileStat', undefined);
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
export const type = new ProtocolRequestType<ReadDirectoryParams, DirectoryEntry[] | null, never, void, void>(method);
export type HandlerSignature = RequestHandler<ReadDirectoryParams, DirectoryEntry[] | null, void>;
export const capabilities = CM.create('workspace.fileOperations.readDirectory', undefined);
export const messageDirection: MessageDirection = MessageDirection.serverToClient;
export const type = new ProtocolRequestType<ReadFileParams, ReadFileResult | null, never, void, void>(method);
export type HandlerSignature = RequestHandler<ReadFileParams, ReadFileResult | null, void>;
export const capabilities = CM.create('workspace.fileOperations.readFile', undefined);
Comment on lines +4396 to +4397
FileStat, StatParams, StatRequest, DirectoryEntry, FileType, FileFlags, ReadDirectoryParams, ReadDirectoryRequest, ReadFileParams, ReadFileRequest, ReadFileResult,
ReadFileParamKind, TextReadFileParams, BinaryReadFileParams,
return result;
}

function asFileType(value: code.FileType): {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants