diff --git a/package.json b/package.json index 2f37c5955..960cb615c 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,12 @@ "onCommand:metals.new-scala-project", "onDebugResolve:scala", "onLanguage:scala", - "workspaceContains:build.sbt", - "workspaceContains:build.sc", - "workspaceContains:project/build.properties", - "workspaceContains:src/main/scala", - "workspaceContains:*/src/main/scala", - "workspaceContains:*/*/src/main/scala" + "!onFileSystem:jar && workspaceContains:build.sbt", + "!onFileSystem:jar && workspaceContains:build.sc", + "!onFileSystem:jar && workspaceContains:project/build.properties", + "!onFileSystem:jar && workspaceContains:src/main/scala", + "!onFileSystem:jar && workspaceContains:*/src/main/scala", + "!onFileSystem:jar && workspaceContains:*/*/src/main/scala" ], "contributes": { "languages": [ diff --git a/src/extension.ts b/src/extension.ts index 5dba179f6..1bc149a25 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -28,6 +28,7 @@ import { ProviderResult, Hover, TextDocument, + FileSystemProvider, } from "vscode"; import { LanguageClient, @@ -90,7 +91,7 @@ import { BuildTargetUpdate } from "./testExplorer/types"; import * as workbenchCommands from "./workbenchCommands"; import { getServerVersion } from "./getServerVersion"; import { getCoursierMirrorPath } from "./mirrors"; - +import MetalsFileSystemProvider from "./metalsFileSystemProvider"; const outputChannel = window.createOutputChannel("Metals"); const openSettingsAction = "Open settings"; const downloadJava = "Download Java"; @@ -228,7 +229,7 @@ function fetchAndLaunchMetals( if (dottyIde.enabled) { outputChannel.appendLine( `Metals will not start since Dotty is enabled for this workspace. ` + - `To enable Metals, remove the file ${dottyIde.path} and run 'Reload window'` + `To enable Metals, remove the file ${dottyIde.path} and run 'Reload window'` ); return; } @@ -441,6 +442,15 @@ function launchMetals( ); } + function registerFileSystemProvider( + scheme: string, + provider: FileSystemProvider + ) { + context.subscriptions.push( + workspace.registerFileSystemProvider(scheme, provider, { isCaseSensitive: true, isReadonly: true }) + ); + } + function registerTextDocumentContentProvider( scheme: string, provider: TextDocumentContentProvider @@ -453,7 +463,6 @@ function launchMetals( const metalsFileProvider = new MetalsFileProvider(client); registerTextDocumentContentProvider("metalsDecode", metalsFileProvider); - registerTextDocumentContentProvider("jar", metalsFileProvider); registerCommand("metals.show-cfr", async (uri: Uri) => { await decodeAndShowFile(client, metalsFileProvider, uri, "cfr"); @@ -730,6 +739,19 @@ function launchMetals( } break; } + case "metals-create-library-filesystem": { + const uri = params.arguments && params.arguments[0]; + if (typeof uri === "string") { + const librariesURI = Uri.parse(uri) + // filesystem is persistent across VSCode sessions so may already exist + if (!workspace.getWorkspaceFolder(librariesURI)) + workspace.updateWorkspaceFolders(1, 0, { uri: librariesURI, name: "Metals - Libraries" }); + + const metalsFileSystemProvider = new MetalsFileSystemProvider(client, outputChannel); + registerFileSystemProvider(librariesURI.scheme, metalsFileSystemProvider); + } + break; + } case ClientCommands.FocusDiagnostics: commands.executeCommand(ClientCommands.FocusDiagnostics); break; diff --git a/src/metalsFileSystemProvider.ts b/src/metalsFileSystemProvider.ts new file mode 100644 index 000000000..8334b1d5e --- /dev/null +++ b/src/metalsFileSystemProvider.ts @@ -0,0 +1,131 @@ +//import { ServerCommands } from "metals-languageclient"; +import { + EventEmitter, + //ProviderResult, + FileSystemProvider, + Uri, + Disposable, + Event, + FileChangeEvent, + FileStat, + FileType, + OutputChannel, +} from "vscode"; +import { ExecuteCommandRequest } from "vscode-languageclient"; +import { LanguageClient } from "vscode-languageclient/node"; + + +export interface FSReadDirectoryResponse { + name: string, + isFile: boolean, + error?: string +} + +export interface FSReadFileResponse { + value: string, + error?: string +} + +export interface FSStatResponse { + isFile: boolean, + error?: string +} + +export class GenericFileStat implements FileStat { + type: FileType; + ctime: number; + mtime: number; + size: number; + + constructor(isFile: boolean) { + this.type = (isFile) ? FileType.File : FileType.Directory; + this.ctime = Date.now(); + this.mtime = Date.now(); + this.size = 0; + } +} + +export default class MetalsFileSystemProvider implements FileSystemProvider { + readonly onDidChangeEmitter = new EventEmitter(); + readonly onDidChange = this.onDidChangeEmitter.event; + readonly client: LanguageClient; + readonly outputChannel: OutputChannel; + + constructor(client: LanguageClient, + outputChannel: OutputChannel) { + this.outputChannel = outputChannel; + this.client = client; + } + + private _emitter = new EventEmitter(); + readonly onDidChangeFile: Event = this._emitter.event; + + stat(uri: Uri): FileStat | Thenable { + return this.client.sendRequest(ExecuteCommandRequest.type, { + // TODO move to ServerCommands + command: "filesystem-stat", + arguments: [uri.toString(true)], + }).then((result) => { + // TODO how much info is required here? + const value = result as FSStatResponse; + return new GenericFileStat(value.isFile); + }); + } + + toReadDirResult(response: FSReadDirectoryResponse): [string, FileType] { + return [response.name, response.isFile ? FileType.File : FileType.Directory] + } + + readDirectory(uri: Uri): [string, FileType][] | Thenable<[string, FileType][]> { + return this.client.sendRequest(ExecuteCommandRequest.type, { + // TODO move to ServerCommands + command: "filesystem-read-directory", + arguments: [uri.toString(true)], + }).then((result) => { + const value = result as FSReadDirectoryResponse[]; + return value.map(this.toReadDirResult); + }); + } + + readFile(uri: Uri): Uint8Array | Thenable { + return this.client.sendRequest(ExecuteCommandRequest.type, { + // TODO move to ServerCommands + command: "filesystem-read-file", + arguments: [uri.toString(true)], + }).then((result) => { + const { value, error } = result as FSReadFileResponse; + let contents: string; + if (value != null) { + contents = value; + } else { + if (error) + contents = error; + else + contents = "Unknown error"; + } + return Buffer.from(contents); + }); + } + + + watch(uri: Uri, options: { recursive: boolean; excludes: string[]; }): Disposable { + this.outputChannel.appendLine(`ignoring watch ${uri} ${options}`); + throw new Error(`watch ${uri} ${options} not implemented.`); + } + createDirectory(uri: Uri): void | Thenable { + this.outputChannel.appendLine(`ignoring createDirectory ${uri}`); + throw new Error(`createDirectory ${uri} not implemented.`); + } + delete(uri: Uri, options: { recursive: boolean; }): void | Thenable { + this.outputChannel.appendLine(`ignoring delete ${uri} ${options}`); + throw new Error(`delete ${uri} ${options} not implemented.`); + } + rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean; }): void | Thenable { + this.outputChannel.appendLine(`ignoring rename ${oldUri} ${newUri} ${options}`); + throw new Error(`rename ${oldUri} ${newUri} ${options} not implemented.`); + } + writeFile(uri: Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Thenable { + this.outputChannel.appendLine(`ignoring writeFile ${uri} ${content} ${options}`); + throw new Error(`writeFile ${uri} ${content} ${options} not implemented.`); + } +}