diff --git a/client/src/commands.ts b/client/src/commands.ts index 4c00b9e38..a50a44dae 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -1,40 +1,12 @@ -import * as fs from "fs"; -import { window, DiagnosticCollection } from "vscode"; -import { LanguageClient, RequestType } from "vscode-languageclient/node"; +import { DiagnosticCollection } from "vscode"; + import { DiagnosticsResultCodeActionsMap, runDeadCodeAnalysisWithReanalyze, } from "./commands/dead_code_analysis"; -interface CreateInterfaceRequestParams { - uri: string; -} - -let createInterfaceRequest = new RequestType< - CreateInterfaceRequestParams, - string, - void ->("rescript-vscode.create_interface"); - -export const createInterface = (client: LanguageClient) => { - if (!client) { - return window.showInformationMessage("Language server not running"); - } - - const editor = window.activeTextEditor; - - if (!editor) { - return window.showInformationMessage("No active editor"); - } - - if (fs.existsSync(editor.document.uri.fsPath + "i")) { - return window.showInformationMessage("Interface file already exists"); - } - - client.sendRequest(createInterfaceRequest, { - uri: editor.document.uri.toString(), - }); -}; +export { createInterface } from "./commands/create_interface"; +export { openCompiled } from "./commands/open_compiled"; export const deadCodeAnalysisWithReanalyze = ( targetDir: string | null, diff --git a/client/src/commands/create_interface.ts b/client/src/commands/create_interface.ts new file mode 100644 index 000000000..dd045e8ad --- /dev/null +++ b/client/src/commands/create_interface.ts @@ -0,0 +1,33 @@ +import * as fs from "fs"; +import { LanguageClient, RequestType } from "vscode-languageclient/node"; +import { window } from "vscode"; + +interface CreateInterfaceRequestParams { + uri: string; +} + +let createInterfaceRequest = new RequestType< + CreateInterfaceRequestParams, + string, + void +>("rescript-vscode.create_interface"); + +export const createInterface = (client: LanguageClient) => { + if (!client) { + return window.showInformationMessage("Language server not running"); + } + + const editor = window.activeTextEditor; + + if (!editor) { + return window.showInformationMessage("No active editor"); + } + + if (fs.existsSync(editor.document.uri.fsPath + "i")) { + return window.showInformationMessage("Interface file already exists"); + } + + client.sendRequest(createInterfaceRequest, { + uri: editor.document.uri.toString(), + }); +}; diff --git a/client/src/commands/open_compiled.ts b/client/src/commands/open_compiled.ts new file mode 100644 index 000000000..5ba7d0a8e --- /dev/null +++ b/client/src/commands/open_compiled.ts @@ -0,0 +1,45 @@ +import * as fs from "fs"; +import { window, Uri, ViewColumn } from "vscode"; +import { LanguageClient, RequestType } from "vscode-languageclient/node"; + +interface OpenCompiledFileRequestParams { + uri: string; +} + +interface OpenCompiledFileResponseParams { + uri: string; +} + +let openCompiledFileRequest = new RequestType< + OpenCompiledFileRequestParams, + OpenCompiledFileResponseParams, + void +>("rescript-vscode.open_compiled"); + +export const openCompiled = (client: LanguageClient) => { + if (!client) { + return window.showInformationMessage("Language server not running"); + } + + const editor = window.activeTextEditor; + + if (!editor) { + return window.showInformationMessage("No active editor"); + } + + if (!fs.existsSync(editor.document.uri.fsPath)) { + return window.showInformationMessage("Compiled file does not exist"); + } + + client + .sendRequest(openCompiledFileRequest, { + uri: editor.document.uri.toString(), + }) + .then((response) => { + const document = Uri.file(response.uri); + + return window.showTextDocument(document, { + viewColumn: ViewColumn.Beside, + }); + }); +}; diff --git a/client/src/extension.ts b/client/src/extension.ts index 36a0e5296..8b8c18620 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -146,6 +146,10 @@ export function activate(context: ExtensionContext) { customCommands.createInterface(client); }); + commands.registerCommand("rescript-vscode.open_compiled", () => { + customCommands.openCompiled(client); + }); + // Starts the dead code analysis mode. commands.registerCommand("rescript-vscode.start_dead_code_analysis", () => { // Save the directory this first ran from, and re-use that when continuously diff --git a/package.json b/package.json index dd762a0da..5c636a639 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,26 @@ "command": "rescript-vscode.create_interface", "title": "ReScript: Create an interface file for this implementation file." }, + { + "command": "rescript-vscode.open_compiled", + "category": "ReScript", + "title": "Open the compiled JS file for this implementation file.", + "icon": "$(output)" + }, { "command": "rescript-vscode.start_dead_code_analysis", "title": "ReScript: Start dead code analysis." } ], + "menus": { + "editor/title": [ + { + "command": "rescript-vscode.open_compiled", + "when": "editorLangId == rescript", + "group": "navigation" + } + ] + }, "snippets": [ { "language": "rescript", diff --git a/server/src/constants.ts b/server/src/constants.ts index d0f8342f7..498b98b6a 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -45,3 +45,7 @@ export let resExt = ".res"; export let resiExt = ".resi"; export let cmiExt = ".cmi"; export let startBuildAction = "Start Build"; + +// bsconfig defaults according configuration schema (https://rescript-lang.org/docs/manual/latest/build-configuration-schema) +export let bsconfigModuleDefault = "commonjs"; +export let bsconfigSuffixDefault = ".js"; diff --git a/server/src/server.ts b/server/src/server.ts index e2bc71349..a31c2a6b1 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -50,6 +50,16 @@ let createInterfaceRequest = string, void>("rescript-vscode.create_interface"); +interface OpenCompiledFileParams { + uri: string; +} + +let openCompiledFileRequest = new v.RequestType< + OpenCompiledFileParams, + OpenCompiledFileParams, + void +>("rescript-vscode.open_compiled"); + let sendUpdatedDiagnostics = () => { projectsFiles.forEach(({ filesWithDiagnostics }, projectRootPath) => { let content = fs.readFileSync( @@ -120,7 +130,7 @@ let sendCompilationFinishedMessage = () => { jsonrpc: c.jsonrpcVersion, method: "rescript/compilationFinished", }; - + send(notification); }; @@ -548,7 +558,7 @@ function createInterface(msg: p.RequestMessage): m.Message { return response; } - let namespace = namespaceResult.result + let namespace = namespaceResult.result; let suffixToAppend = namespace.length > 0 ? "-" + namespace : ""; let cmiPartialPath = path.join( @@ -602,6 +612,64 @@ function createInterface(msg: p.RequestMessage): m.Message { } } +function openCompiledFile(msg: p.RequestMessage): m.Message { + let params = msg.params as OpenCompiledFileParams; + let filePath = fileURLToPath(params.uri); + let projDir = utils.findProjectRootOfFile(filePath); + + if (projDir === null) { + let params: p.ShowMessageParams = { + type: p.MessageType.Error, + message: `Cannot locate project directory.`, + }; + + let response: m.NotificationMessage = { + jsonrpc: c.jsonrpcVersion, + method: "window/showMessage", + params: params, + }; + + return response; + } + + let compiledFilePath = utils.getCompiledFilePath(filePath, projDir); + + if ( + compiledFilePath.kind === "error" || + !fs.existsSync(compiledFilePath.result) + ) { + let message = + compiledFilePath.kind === "success" + ? `No compiled file found. Expected it at: ${compiledFilePath.result}` + : `No compiled file found. Please compile your project first.`; + + let params: p.ShowMessageParams = { + type: p.MessageType.Error, + message, + }; + + let response: m.NotificationMessage = { + jsonrpc: c.jsonrpcVersion, + method: "window/showMessage", + params, + }; + + return response; + } + + let result: OpenCompiledFileParams = { + uri: compiledFilePath.result, + }; + + let response: m.ResponseMessage = { + jsonrpc: c.jsonrpcVersion, + id: msg.id, + result, + }; + + return response; +} + function onMessage(msg: m.Message) { if (m.isNotificationMessage(msg)) { // notification message, aka the client ends it and doesn't want a reply @@ -734,6 +802,8 @@ function onMessage(msg: m.Message) { responses.forEach((response) => send(response)); } else if (msg.method === createInterfaceRequest.method) { send(createInterface(msg)); + } else if (msg.method === openCompiledFileRequest.method) { + send(openCompiledFile(msg)); } else { let response: m.ResponseMessage = { jsonrpc: c.jsonrpcVersion, diff --git a/server/src/utils.ts b/server/src/utils.ts index 9f3fe1cf9..462c058ee 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -186,34 +186,43 @@ export const toCamelCase = (text: string): string => { .replace(/(\s|-)+/g, ""); }; -export const getNamespaceNameFromBsConfig = ( - projDir: p.DocumentUri -): execResult => { +const readBsConfig = (projDir: p.DocumentUri) => { try { let bsconfigFile = fs.readFileSync( path.join(projDir, c.bsconfigPartialPath), { encoding: "utf-8" } ); - let bsconfig = JSON.parse(bsconfigFile); - let result = ""; + let result = JSON.parse(bsconfigFile); + return result; + } catch (e) { + return null; + } +}; - if (bsconfig.namespace === true) { - result = toCamelCase(bsconfig.name); - } else if (typeof bsconfig.namespace === "string") { - result = toCamelCase(bsconfig.namespace); - } +export const getNamespaceNameFromBsConfig = ( + projDir: p.DocumentUri +): execResult => { + let bsconfig = readBsConfig(projDir); + let result = ""; - return { - kind: "success", - result, - }; - } catch (e) { + if (!bsconfig) { return { kind: "error", - error: e instanceof Error ? e.message : String(e), + error: "Could not read bsconfig", }; } + + if (bsconfig.namespace === true) { + result = toCamelCase(bsconfig.name); + } else if (typeof bsconfig.namespace === "string") { + result = toCamelCase(bsconfig.namespace); + } + + return { + kind: "success", + result, + }; }; export let createInterfaceFileUsingValidBscExePath = ( @@ -243,6 +252,76 @@ export let createInterfaceFileUsingValidBscExePath = ( } }; +let getCompiledFolderName = (moduleFormat: string): string => { + switch (moduleFormat) { + case "es6": + return "es6"; + case "es6-global": + return "es6_global"; + case "commonjs": + default: + return "js"; + } +}; + +export let getCompiledFilePath = ( + filePath: string, + projDir: string +): execResult => { + let bsconfig = readBsConfig(projDir); + + if (!bsconfig) { + return { + kind: "error", + error: "Could not read bsconfig", + }; + } + + let pkgSpecs = bsconfig["package-specs"]; + let pathFragment = ""; + let moduleFormatObj: any = {}; + + let module = c.bsconfigModuleDefault; + let suffix = c.bsconfigSuffixDefault; + + if (pkgSpecs) { + if (pkgSpecs.module) { + moduleFormatObj = pkgSpecs; + } else if (typeof pkgSpecs === "string") { + module = pkgSpecs; + } else if (pkgSpecs[0]) { + if (typeof pkgSpecs[0] === "string") { + module = pkgSpecs[0]; + } else { + moduleFormatObj = pkgSpecs[0]; + } + } + } + + if (moduleFormatObj["module"]) { + module = moduleFormatObj["module"]; + } + + if (!moduleFormatObj["in-source"]) { + pathFragment = "lib/" + getCompiledFolderName(module); + } + + if (moduleFormatObj.suffix) { + suffix = moduleFormatObj.suffix; + } else if (bsconfig.suffix) { + suffix = bsconfig.suffix; + } + + let partialFilePath = filePath.split(projDir)[1]; + let compiledPartialPath = replaceFileExtension(partialFilePath, suffix); + let result = path.join(projDir, pathFragment, compiledPartialPath); + + return { + kind: "success", + result, + }; +}; + export let runBuildWatcherUsingValidBuildPath = ( buildPath: p.DocumentUri, isRescript: boolean,