Skip to content

Commit

Permalink
feat: use new filesystem requests
Browse files Browse the repository at this point in the history
  • Loading branch information
Loïc Mangeonjean committed Mar 7, 2024
1 parent 05bd15a commit 3976cf8
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 62 deletions.
73 changes: 49 additions & 24 deletions src/customRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,65 @@ export interface WillShutdownParams {
}
export const willShutdownNotificationType = new ProtocolNotificationType<WillShutdownParams, void>('willShutdown')

export interface SaveTextDocumentParams {
textDocument: {
uri: string
text: string
}
export interface WriteFileParams {
uri: string
content: string
}

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

export interface GetTextDocumentParams {
textDocument: {
uri: string
}
export interface ReadFileParams {
uri: string
}

export interface GetTextDocumentResult {
text: string
export interface ReadFileResult {
content: string
}

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

export function updateFile (uri: string, text: string, languageClient: LanguageClient): Promise<void> {
return languageClient.sendRequest(saveTextDocumentRequestType, {
textDocument: {
uri,
text
}
export interface StatFileParams {
uri: string
}
export interface StatFileResult {
type: 'directory' | 'file'
size: number
name: string
mtime: number
}

export const getFileStatsRequestType = new ProtocolRequestType<StatFileParams, StatFileResult, never, void, void>('file/stats')

export interface ListFilesParams {
directory: string
}
export interface ListFilesResult {
files: string[]
}

export const listFileRequestType = new ProtocolRequestType<ListFilesParams, ListFilesResult, never, void, void>('file/readdir')

export function writeFile (uri: string, content: string, languageClient: LanguageClient): Promise<void> {
return languageClient.sendRequest(writeFileRequestType, {
uri,
content
})
}

export function readFile (uri: string, languageClient: LanguageClient): Promise<ReadFileResult> {
return languageClient.sendRequest(readFileRequestType, {
uri
})
}

export function getFileStats (uri: string, languageClient: LanguageClient): Promise<StatFileResult> {
return languageClient.sendRequest(getFileStatsRequestType, {
uri
})
}

export function getFile (uri: string, languageClient: LanguageClient): Promise<{ text: string }> {
return languageClient.sendRequest(getTextDocumentRequestType, {
textDocument: {
uri
}
export function listFiles (directory: string, languageClient: LanguageClient): Promise<ListFilesResult> {
return languageClient.sendRequest(listFileRequestType, {
directory
})
}
92 changes: 65 additions & 27 deletions src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@ import { Infrastructure } from './infrastructure'
import { LanguageClientManager } from './languageClient'
import { MonacoLanguageClient } from './createLanguageClient'

async function bufferToBase64 (buffer: ArrayBuffer | Uint8Array) {
// use a FileReader to generate a base64 data URI:
const base64url = await new Promise<string>((resolve) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.readAsDataURL(new Blob([buffer]))
})
// remove the `data:...;base64,` part from the start
return base64url.slice(base64url.indexOf(',') + 1)
}

async function base64ToBufferAsync (base64: string) {
const dataUrl = `data:application/octet-binary;base64,${base64}`

const result = await fetch(dataUrl)
const buffer = await result.arrayBuffer()
return new Uint8Array(buffer)
}

interface ResolvedTextDocumentSyncCapabilities {
resolvedTextDocumentSync?: TextDocumentSyncOptions
}
Expand All @@ -31,7 +50,7 @@ export class InitializeTextDocumentFeature implements StaticFeature {
const languageClient = this.languageClient
async function saveFile (textDocument: vscode.TextDocument) {
if (documentSelector != null && vscode.languages.match(documentSelector, textDocument) > 0 && textDocument.uri.scheme === 'file') {
await infrastructure.saveFileContent?.(textDocument.uri, textDocument.getText(), languageClient)
await infrastructure.writeFile?.(textDocument.uri, btoa(textDocument.getText()), languageClient)

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

const encoder = new TextEncoder()
const decoder = new TextDecoder()
class InfrastructureTextFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
class InfrastructureFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.Readonly
constructor (private infrastructure: Infrastructure, private languageClientManager: LanguageClientManager) {
}

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

const blacklisted = REMOTE_FILE_BLACKLIST.some(blacklisted => resource.path.endsWith(blacklisted))
if (blacklisted) {
return undefined
}
if (!this.cachedContent.has(resource.toString())) {
this.cachedContent.set(resource.toString(), this.infrastructure.getFileContent!(resource, this.languageClientManager))
}
return await this.cachedContent.get(resource.toString())
return blacklisted
}

async readFile (resource: monaco.Uri): Promise<Uint8Array> {
const content = await this.getFileContent(resource)
return encoder.encode(content)
if (this.isBlacklisted(resource)) {
throw FileSystemProviderError.create('Not allowed', FileSystemProviderErrorCode.NoPermissions)
}
try {
const file = await this.infrastructure.readFile!(resource, this.languageClientManager)
return await base64ToBufferAsync(file)
} catch (err) {
if ((err as Error).message === 'File not found') {
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.FileNotFound)
}
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.Unknown)
}
}

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

async stat (resource: monaco.Uri): Promise<IStat> {
try {
const content = await this.getFileContent(resource)
if (content != null) {
if (this.isBlacklisted(resource)) {
throw FileSystemProviderError.create('Not allowed', FileSystemProviderErrorCode.NoPermissions)
}
const fileStats = await this.infrastructure.getFileStats?.(resource, this.languageClientManager)
if (fileStats != null) {
return {
type: FileType.File,
size: encoder.encode(content).length,
mtime: Date.now(),
ctime: Date.now()
type: fileStats.type === 'directory' ? FileType.Directory : FileType.File,
size: fileStats.size,
mtime: fileStats.mtime,
ctime: 0
}
}
} catch (err) {
if ((err as Error).message === 'File not found') {
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.FileNotFound)
}
throw FileSystemProviderError.create(err as Error, FileSystemProviderErrorCode.Unknown)
}
throw FileSystemProviderError.create('file not found', FileSystemProviderErrorCode.FileNotFound)
Expand All @@ -134,8 +160,20 @@ class InfrastructureTextFileSystemProvider implements IFileSystemProviderWithFil
async mkdir (): Promise<void> {
}

async readdir () {
return []
async readdir (resource: monaco.Uri) {
const result = await this.infrastructure.listFiles?.(resource, this.languageClientManager)
if (result == null) {
return []
}
return result.map(file => {
let name = file
let type = FileType.File
if (file.endsWith('/')) {
type = FileType.Directory
name = file.slice(0, -1)
}
return <[string, FileType]>[name, type]
})
}

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

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

if (this.infrastructure.saveFileContent != null) {
if (this.infrastructure.writeFile != null) {
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(this.infrastructure.rootUri), '**/*'))
disposables.add(watcher)
const onFileChange = async (uri: vscode.Uri) => {
if ((await vscode.workspace.fs.stat(uri)).type === vscode.FileType.File) {
const content = await vscode.workspace.fs.readFile(uri)
await this.infrastructure.saveFileContent?.(uri, decoder.decode(content), this.languageClientManager)
await this.infrastructure.writeFile?.(uri, await bufferToBase64(content), this.languageClientManager)
}
}
watcher.onDidChange(onFileChange)
Expand Down
40 changes: 30 additions & 10 deletions src/infrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MessageTransports } from 'vscode-languageclient'
import * as monaco from 'monaco-editor'
import * as vscode from 'vscode'
import { LSPAny } from 'vscode-languageserver-protocol'
import { getFile, updateFile } from './customRequests'
import { StatFileResult, getFileStats, listFiles, writeFile, readFile } from './customRequests'
import { LanguageClientManager } from './languageClient'
import { LanguageClientId, LanguageClientOptions } from './languageClientOptions'

Expand Down Expand Up @@ -31,16 +31,28 @@ export interface Infrastructure {
/**
* Save a file on the filesystem
* @param document The document to save
* @param reason The reason of the save
* @param content The content of the file in base64
* @param languageClient The languageclient we're trying to save the file to
*/
saveFileContent? (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void>
writeFile? (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void>
/**
* Get a text file content as a model
* Get a text file content
* @param resource the Uri of the file
* @param languageClient The languageclient we're trying to get the file from
*/
getFileContent? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string | undefined>
readFile? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string>
/**
* Get file stats on a given file
* @param resource the Uri of the file
* @param languageClient The languageclient we're trying to get the file from
*/
getFileStats? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<StatFileResult>
/**
* List the files of a directory
* @param resource the Uri of the directory
* @param languageClient The languageclient we're trying to get the file from
*/
listFiles? (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string[]>

/**
* Open a connection to the language server
Expand Down Expand Up @@ -106,17 +118,25 @@ export abstract class CodinGameInfrastructure implements Infrastructure {
name: 'main'
}]

public async saveFileContent (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void> {
public async writeFile (document: monaco.Uri, content: string, languageClient: LanguageClientManager): Promise<void> {
if (languageClient.isConnected()) {
await updateFile(document.toString(), content, languageClient)
await writeFile(document.toString(), content, languageClient)
}
}

public async getFileContent (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string | undefined> {
public async readFile (resource: monaco.Uri, languageClient: LanguageClientManager): Promise<string> {
return (await readFile(resource.toString(true), languageClient)).content
}

public async getFileStats (directory: monaco.Uri, languageClient: LanguageClientManager): Promise<StatFileResult> {
return (await getFileStats(directory.toString(true), languageClient))
}

public async listFiles (directory: monaco.Uri, languageClient: LanguageClientManager): Promise<string[]> {
try {
return (await getFile(resource.toString(true), languageClient)).text
return (await listFiles(directory.toString(true), languageClient)).files
} catch (error) {
return undefined
return []
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/languageClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export class LanguageClientManager implements LanguageClient {

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

if (this.infrastructure.getFileContent != null) {
if (this.infrastructure.readFile != null) {
this.languageClient.registerFeature(new FileSystemFeature(this.infrastructure, this))
}

Expand Down

0 comments on commit 3976cf8

Please sign in to comment.