Skip to content

Commit 79f5538

Browse files
author
Loïc Mangeonjean
committed
feat: use new filesystem requests
1 parent a68f11a commit 79f5538

File tree

4 files changed

+145
-62
lines changed

4 files changed

+145
-62
lines changed

src/customRequests.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,65 @@ export interface WillShutdownParams {
66
}
77
export const willShutdownNotificationType = new ProtocolNotificationType<WillShutdownParams, void>('willShutdown')
88

9-
export interface SaveTextDocumentParams {
10-
textDocument: {
11-
uri: string
12-
text: string
13-
}
9+
export interface WriteFileParams {
10+
uri: string
11+
content: string
1412
}
1513

16-
export const saveTextDocumentRequestType = new ProtocolRequestType<SaveTextDocumentParams, void, never, void, void>('textDocument/save')
14+
export const writeFileRequestType = new ProtocolRequestType<WriteFileParams, void, never, void, void>('file/write')
1715

18-
export interface GetTextDocumentParams {
19-
textDocument: {
20-
uri: string
21-
}
16+
export interface ReadFileParams {
17+
uri: string
2218
}
2319

24-
export interface GetTextDocumentResult {
25-
text: string
20+
export interface ReadFileResult {
21+
content: string
2622
}
2723

28-
export const getTextDocumentRequestType = new ProtocolRequestType<GetTextDocumentParams, GetTextDocumentResult, never, void, void>('textDocument/get')
24+
export const readFileRequestType = new ProtocolRequestType<ReadFileParams, ReadFileResult, never, void, void>('file/read')
2925

30-
export function updateFile (uri: string, text: string, languageClient: LanguageClient): Promise<void> {
31-
return languageClient.sendRequest(saveTextDocumentRequestType, {
32-
textDocument: {
33-
uri,
34-
text
35-
}
26+
export interface StatFileParams {
27+
uri: string
28+
}
29+
export interface StatFileResult {
30+
type: 'directory' | 'file'
31+
size: number
32+
name: string
33+
mtime: number
34+
}
35+
36+
export const getFileStatsRequestType = new ProtocolRequestType<StatFileParams, StatFileResult, never, void, void>('file/stats')
37+
38+
export interface ListFilesParams {
39+
directory: string
40+
}
41+
export interface ListFilesResult {
42+
files: string[]
43+
}
44+
45+
export const listFileRequestType = new ProtocolRequestType<ListFilesParams, ListFilesResult, never, void, void>('file/readdir')
46+
47+
export function writeFile (uri: string, content: string, languageClient: LanguageClient): Promise<void> {
48+
return languageClient.sendRequest(writeFileRequestType, {
49+
uri,
50+
content
51+
})
52+
}
53+
54+
export function readFile (uri: string, languageClient: LanguageClient): Promise<ReadFileResult> {
55+
return languageClient.sendRequest(readFileRequestType, {
56+
uri
57+
})
58+
}
59+
60+
export function getFileStats (uri: string, languageClient: LanguageClient): Promise<StatFileResult> {
61+
return languageClient.sendRequest(getFileStatsRequestType, {
62+
uri
3663
})
3764
}
3865

39-
export function getFile (uri: string, languageClient: LanguageClient): Promise<{ text: string }> {
40-
return languageClient.sendRequest(getTextDocumentRequestType, {
41-
textDocument: {
42-
uri
43-
}
66+
export function listFiles (directory: string, languageClient: LanguageClient): Promise<ListFilesResult> {
67+
return languageClient.sendRequest(listFileRequestType, {
68+
directory
4469
})
4570
}

src/extensions.ts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@ import { Infrastructure } from './infrastructure'
1010
import { LanguageClientManager } from './languageClient'
1111
import { MonacoLanguageClient } from './createLanguageClient'
1212

13+
async function bufferToBase64 (buffer: ArrayBuffer | Uint8Array) {
14+
// use a FileReader to generate a base64 data URI:
15+
const base64url = await new Promise<string>((resolve) => {
16+
const reader = new FileReader()
17+
reader.onload = () => resolve(reader.result as string)
18+
reader.readAsDataURL(new Blob([buffer]))
19+
})
20+
// remove the `data:...;base64,` part from the start
21+
return base64url.slice(base64url.indexOf(',') + 1)
22+
}
23+
24+
async function base64ToBufferAsync (base64: string) {
25+
const dataUrl = `data:application/octet-binary;base64,${base64}`
26+
27+
const result = await fetch(dataUrl)
28+
const buffer = await result.arrayBuffer()
29+
return new Uint8Array(buffer)
30+
}
31+
1332
interface ResolvedTextDocumentSyncCapabilities {
1433
resolvedTextDocumentSync?: TextDocumentSyncOptions
1534
}
@@ -31,7 +50,7 @@ export class InitializeTextDocumentFeature implements StaticFeature {
3150
const languageClient = this.languageClient
3251
async function saveFile (textDocument: vscode.TextDocument) {
3352
if (documentSelector != null && vscode.languages.match(documentSelector, textDocument) > 0 && textDocument.uri.scheme === 'file') {
34-
await infrastructure.saveFileContent?.(textDocument.uri, textDocument.getText(), languageClient)
53+
await infrastructure.writeFile?.(textDocument.uri, btoa(textDocument.getText()), languageClient)
3554

3655
// Always send notification even if the server doesn't support it (because csharp register the didSave feature too late)
3756
await languageClient.sendNotification(DidSaveTextDocumentNotification.type, {
@@ -76,30 +95,31 @@ export class WillDisposeFeature implements StaticFeature {
7695
clear (): void {}
7796
}
7897

79-
const encoder = new TextEncoder()
80-
const decoder = new TextDecoder()
81-
class InfrastructureTextFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
98+
class InfrastructureFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
8299
capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.Readonly
83100
constructor (private infrastructure: Infrastructure, private languageClientManager: LanguageClientManager) {
84101
}
85102

86-
private cachedContent: Map<string, Promise<string | undefined>> = new Map()
87-
private async getFileContent (resource: monaco.Uri): Promise<string | undefined> {
103+
private isBlacklisted (resource: monaco.Uri) {
88104
const REMOTE_FILE_BLACKLIST = ['.git/config', '.vscode', monaco.Uri.parse(this.infrastructure.rootUri).path]
89105

90106
const blacklisted = REMOTE_FILE_BLACKLIST.some(blacklisted => resource.path.endsWith(blacklisted))
91-
if (blacklisted) {
92-
return undefined
93-
}
94-
if (!this.cachedContent.has(resource.toString())) {
95-
this.cachedContent.set(resource.toString(), this.infrastructure.getFileContent!(resource, this.languageClientManager))
96-
}
97-
return await this.cachedContent.get(resource.toString())
107+
return blacklisted
98108
}
99109

100110
async readFile (resource: monaco.Uri): Promise<Uint8Array> {
101-
const content = await this.getFileContent(resource)
102-
return encoder.encode(content)
111+
if (this.isBlacklisted(resource)) {
112+
throw FileSystemProviderError.create('Not allowed', FileSystemProviderErrorCode.NoPermissions)
113+
}
114+
try {
115+
const file = await this.infrastructure.readFile!(resource, this.languageClientManager)
116+
return await base64ToBufferAsync(file)
117+
} catch (err) {
118+
if ((err as Error).message === 'File not found') {
119+
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.FileNotFound)
120+
}
121+
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.Unknown)
122+
}
103123
}
104124

105125
async writeFile (): Promise<void> {
@@ -116,16 +136,22 @@ class InfrastructureTextFileSystemProvider implements IFileSystemProviderWithFil
116136

117137
async stat (resource: monaco.Uri): Promise<IStat> {
118138
try {
119-
const content = await this.getFileContent(resource)
120-
if (content != null) {
139+
if (this.isBlacklisted(resource)) {
140+
throw FileSystemProviderError.create('Not allowed', FileSystemProviderErrorCode.NoPermissions)
141+
}
142+
const fileStats = await this.infrastructure.getFileStats?.(resource, this.languageClientManager)
143+
if (fileStats != null) {
121144
return {
122-
type: FileType.File,
123-
size: encoder.encode(content).length,
124-
mtime: Date.now(),
125-
ctime: Date.now()
145+
type: fileStats.type === 'directory' ? FileType.Directory : FileType.File,
146+
size: fileStats.size,
147+
mtime: fileStats.mtime,
148+
ctime: 0
126149
}
127150
}
128151
} catch (err) {
152+
if ((err as Error).message === 'File not found') {
153+
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.FileNotFound)
154+
}
129155
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.Unknown)
130156
}
131157
throw FileSystemProviderError.create('file not found', FileSystemProviderErrorCode.FileNotFound)
@@ -134,8 +160,20 @@ class InfrastructureTextFileSystemProvider implements IFileSystemProviderWithFil
134160
async mkdir (): Promise<void> {
135161
}
136162

137-
async readdir () {
138-
return []
163+
async readdir (resource: monaco.Uri) {
164+
const result = await this.infrastructure.listFiles?.(resource, this.languageClientManager)
165+
if (result == null) {
166+
return []
167+
}
168+
return result.map(file => {
169+
let name = file
170+
let type = FileType.File
171+
if (file.endsWith('/')) {
172+
type = FileType.Directory
173+
name = file.slice(0, -1)
174+
}
175+
return <[string, FileType]>[name, type]
176+
})
139177
}
140178

141179
delete (): Promise<void> {
@@ -156,17 +194,17 @@ export class FileSystemFeature implements StaticFeature {
156194
const disposables = new DisposableStore()
157195

158196
// Register readonly file system overlay to access remote files
159-
if (this.infrastructure.getFileContent != null) {
160-
disposables.add(registerFileSystemOverlay(-1, new InfrastructureTextFileSystemProvider(this.infrastructure, this.languageClientManager)))
197+
if (this.infrastructure.readFile != null) {
198+
disposables.add(registerFileSystemOverlay(-1, new InfrastructureFileSystemProvider(this.infrastructure, this.languageClientManager)))
161199
}
162200

163-
if (this.infrastructure.saveFileContent != null) {
201+
if (this.infrastructure.writeFile != null) {
164202
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(this.infrastructure.rootUri), '**/*'))
165203
disposables.add(watcher)
166204
const onFileChange = async (uri: vscode.Uri) => {
167205
if ((await vscode.workspace.fs.stat(uri)).type === vscode.FileType.File) {
168206
const content = await vscode.workspace.fs.readFile(uri)
169-
await this.infrastructure.saveFileContent?.(uri, decoder.decode(content), this.languageClientManager)
207+
await this.infrastructure.writeFile?.(uri, await bufferToBase64(content), this.languageClientManager)
170208
}
171209
}
172210
watcher.onDidChange(onFileChange)

src/infrastructure.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { MessageTransports } from 'vscode-languageclient'
33
import * as monaco from 'monaco-editor'
44
import * as vscode from 'vscode'
55
import { LSPAny } from 'vscode-languageserver-protocol'
6-
import { getFile, updateFile } from './customRequests'
6+
import { StatFileResult, getFileStats, listFiles, writeFile, readFile } from './customRequests'
77
import { LanguageClientManager } from './languageClient'
88
import { LanguageClientId, LanguageClientOptions } from './languageClientOptions'
99

@@ -31,16 +31,28 @@ export interface Infrastructure {
3131
/**
3232
* Save a file on the filesystem
3333
* @param document The document to save
34-
* @param reason The reason of the save
34+
* @param content The content of the file in base64
3535
* @param languageClient The languageclient we're trying to save the file to
3636
*/
37-
saveFileContent? (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void>
37+
writeFile? (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void>
3838
/**
39-
* Get a text file content as a model
39+
* Get a text file content
4040
* @param resource the Uri of the file
4141
* @param languageClient The languageclient we're trying to get the file from
4242
*/
43-
getFileContent? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string | undefined>
43+
readFile? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string>
44+
/**
45+
* Get file stats on a given file
46+
* @param resource the Uri of the file
47+
* @param languageClient The languageclient we're trying to get the file from
48+
*/
49+
getFileStats? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<StatFileResult>
50+
/**
51+
* List the files of a directory
52+
* @param resource the Uri of the directory
53+
* @param languageClient The languageclient we're trying to get the file from
54+
*/
55+
listFiles? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string[]>
4456

4557
/**
4658
* Open a connection to the language server
@@ -106,17 +118,25 @@ export abstract class CodinGameInfrastructure implements Infrastructure {
106118
name: 'main'
107119
}]
108120

109-
public async saveFileContent (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void> {
121+
public async writeFile (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void> {
110122
if (languageClient.isConnected()) {
111-
await updateFile(document.toString(), content, languageClient)
123+
await writeFile(document.toString(), content, languageClient)
112124
}
113125
}
114126

115-
public async getFileContent (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string | undefined> {
127+
public async readFile (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string> {
128+
return (await readFile(resource.toString(true), languageClient)).content
129+
}
130+
131+
public async getFileStats (directory: monaco.Uri, languageClient: LanguageClientManager): Promise<StatFileResult> {
132+
return (await getFileStats(directory.toString(true), languageClient))
133+
}
134+
135+
public async listFiles (directory: monaco.Uri, languageClient: LanguageClientManager): Promise<string[]> {
116136
try {
117-
return (await getFile(resource.toString(true), languageClient)).text
137+
return (await listFiles(directory.toString(true), languageClient)).files
118138
} catch (error) {
119-
return undefined
139+
return []
120140
}
121141
}
122142

src/languageClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export class LanguageClientManager implements LanguageClient {
324324

325325
this.languageClient.registerFeature(new WillDisposeFeature(this.languageClient, this.onWillShutdownEmitter))
326326

327-
if (this.infrastructure.getFileContent != null) {
327+
if (this.infrastructure.readFile != null) {
328328
this.languageClient.registerFeature(new FileSystemFeature(this.infrastructure, this))
329329
}
330330

0 commit comments

Comments
 (0)