diff --git a/.gitignore b/.gitignore index 17fd15b..e34930d 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,4 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk - +*.vsix diff --git a/README.md b/README.md index 9d90e50..be9b135 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -**If you're on our default workflow, please try [this plugin](https://github.com/jaredly/reason-language-server) instead. Thanks!** - # vscode-reasonml Reason support for Visual Studio Code @@ -12,78 +10,97 @@ There is an `#editorsupport` channel on the Reason [discord server](https://disc ## Features -- highlighting - - [x] advanced syntax highlighting for reason - - [x] basic highlighting for merlin, ocamlbuild, and opam files - -- editing - - [x] document formatting (enable on save with `editor.formatOnSave`) - - [x] completion and snippets - - [x] [rename symbol](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol) (F2 or right click) - - [x] [case splitting](#case-splitting) - -- navigation - - [x] [symbol outline for buffers](https://code.visualstudio.com/docs/editor/editingevolved#_goto-symbol) (⇧⌘O) (type `:` in list to sort items) - - [x] [symbol outline for project](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name) (⌘T) (supports regular expressions) - - [x] [jump-to-definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) (⌃+click) and [code preview](https://code.visualstudio.com/docs/editor/editingevolved#_peek) (⌘+hover) - - [x] find references (⇧F12 or right click) - -- static analysis - - [x] merlin integration with incremental edit synchronization - - [x] display types over definitions (disable with `editor.codeLens` setting) - - [x] display types and markdown-rendered docs on hover - - [x] [online linting and compiler diagnostics with suggested fixes](https://code.visualstudio.com/docs/editor/editingevolved#_errors-warnings) - - ⇧⌘M to toggle diagnostics panel - - F8 to loop through diagnostics for current file - - Click on lightbulb icon for suggested fixes - - [x] built-in support for showing BuckleScript's [bsb](https://bucklescript.github.io/bucklescript/Manual.html#_bucklescript_build_system_code_bsb_code) errors inline, as a companion to merlin's diagnosis. +* highlighting -## Getting Started + * [x] advanced syntax highlighting for reason + * [x] basic highlighting for merlin, ocamlbuild, and opam files -### Recommended Syntax Themes +* editing -Although syntax highlighting should display well in most themes we recommend and test with the following: + * [x] document formatting (enable on save with `editor.formatOnSave`) + * [x] completion and snippets + * [x] [rename symbol](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol) (F2 or right click) -#### Default Themes +* navigation -- Dark+ (*recommended*; this theme is the most thoroughly tested) + * [x] [symbol outline for buffers](https://code.visualstudio.com/docs/editor/editingevolved#_goto-symbol) (⇧⌘O) (type `:` in list to sort items) + * [x] [symbol outline for project](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name) (⌘T) (supports regular expressions) + * [x] [jump-to-definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) (⌃+click) and [code preview](https://code.visualstudio.com/docs/editor/editingevolved#_peek) (⌘+hover) + * [x] find references (⇧F12 or right click) -#### Other Themes +* static analysis + * [x] merlin integration with incremental edit synchronization + * [x] display types over definitions (disable with `editor.codeLens` setting) + * [x] display types and markdown-rendered docs on hover + * [x] [online linting and compiler diagnostics with suggested fixes](https://code.visualstudio.com/docs/editor/editingevolved#_errors-warnings) + * ⇧⌘M to toggle diagnostics panel + * F8 to loop through diagnostics for current file + * Click on lightbulb icon for suggested fixes + * [x] built-in support for showing BuckleScript's [bsb](https://bucklescript.github.io/bucklescript/Manual.html#_bucklescript_build_system_code_bsb_code) errors inline, as a companion to merlin's diagnosis. -- [Atom One Dark](https://marketplace.visualstudio.com/items?itemName=freebroccolo.theme-atom-one-dark) -- [Dracula](https://marketplace.visualstudio.com/items?itemName=dracula-theme.theme-dracula) -- [Flatland Monokai](https://marketplace.visualstudio.com/items?itemName=gerane.Theme-FlatlandMonokai) -- [Oceanic Next](https://marketplace.visualstudio.com/items?itemName=naumovs.theme-oceanicnext) +## Getting Started + +### Configuration + +Extension requires `ocamlmerlin-lsp` (and `ocamlmerlin-reason` if you are using Reason syntax) compatible with the project you are working with. Path to the binary can be specified with `reason.path.ocamlmerlin-lsp` key in your `.vscode/settings`: -### Configurations -#### Reason +```json +"reason.path.ocamlmerlin-dir": "_opam/bin/ocamlmerlin-lsp", +"reason.path.ocamlmerlin-reason": "_opam/bin/ocamlmerlin-reason" +``` + +#### esy -- [Reason](https://reasonml.github.io/docs/en/installation) +If you are using `esy`, you can use this configuration: -The Reason installation steps also installs Merlin for you, so you can skip the Merlin installation in the next section. +````json +"devDependencies": { + "@opam/merlin-lsp": "*", + "@opam/reason": "3.4.0", // if you are using reason + "ocaml": "4.06.x" // version of compiler in your project +}, +"resolutions": { + "@opam/merlin-lsp": "ocaml/merlin:merlin-lsp.opam#f431006" +} -#### Merlin +Then run `esy exec-command which ocamlmerlin-lsp` and `esy exec-command which ocamlmerlin-reason` to get paths to binaries. + +#### bs-platfrom <= 5 + +Bucklescript 5 and earlier using `OCaml 4.02.3` and you will have problems with compliling `merlin-lsp`. But it can be solved with a couple of well placed pins: + +```json +// esy.json +"devDependencies": { + "@opam/merlin-lsp": "*", + "@opam/reason": "3.4.0", + "ocaml": "4.2.x" +}, +"resolutions": { + "@opam/ppx_deriving": { + "source": "github:ocaml-ppx/ppx_deriving:opam#71e61a2", + "override": { + "build": ["ocaml pkg/build.ml native=true native-dynlink=true"] + } + }, + "@opam/merlin-lsp": "github:Khady/merlin:merlin-lsp.opam#9325d1d" +} +``` -**Configured for you already if you've installed Reason above & plan to use it for JS compilation. Skip this step.** +### Recommended Syntax Themes -This extension relies heavily on [merlin](https://github.com/the-lambda-church/merlin) so you will -need to have your project set up for that in order to enable completion and hover info. See the -Merlin [wiki](https://github.com/the-lambda-church/merlin/wiki/project-configuration) for details on -how to do that. Basically you need to have a `.merlin` file in your project root which lists the -source directories, libraries, and extensions used. +Although syntax highlighting should display well in most themes we recommend and test with the following: -#### Bsb +#### Default Themes -You can optionally start [bsb](https://bucklescript.github.io/bucklescript/Manual.html#_bucklescript_build_system_code_bsb_code) from the editor itself, and have the command-line errors appear inside the editor. Add the following to `Code > Preferences > Settings`: +* Dark+ (_recommended_; this theme is the most thoroughly tested) -```reason -"reason.diagnostics.tools": [ - "merlin", - "bsb" -] -``` +#### Other Themes -Merlin's diagnosis is best-effort and can sometimes be wrong; bsb's diagnosis is 100% correct. **bsb diagnosis also works on Windows**. +* [Atom One Dark](https://marketplace.visualstudio.com/items?itemName=freebroccolo.theme-atom-one-dark) +* [Dracula](https://marketplace.visualstudio.com/items?itemName=dracula-theme.theme-dracula) +* [Flatland Monokai](https://marketplace.visualstudio.com/items?itemName=gerane.Theme-FlatlandMonokai) +* [Oceanic Next](https://marketplace.visualstudio.com/items?itemName=naumovs.theme-oceanicnext) ### Installation @@ -102,75 +119,8 @@ To enable formatting on save, add the following to `Code > Preferences > Setting ``` If you want to enable [codelens](https://code.visualstudio.com/blogs/2017/02/12/code-lens-roundup), add the following to `Code > Preferences > Settings`: -``` -"reason.codelens.enabled": true -``` - -## Advanced Features - -### Case splitting -For the examples below, `` represents the position of the current VS Code editor cursor. - -#### Introducing a `switch` - -In order to introduce a `switch`, execute the following steps: - -1. select an identifier or move the cursor anywhere within its word range (as below) -2. open the palette (⇧⌘P) and run `Reason: case split` (typing `case` should pull it up) - -##### Before -``` -let foo (arg: list 'a) => arg; -``` - -##### After -``` -let foo (arg: list 'a) => switch arg { - | [] => failwith "" - | [_, ..._] => failwith "" -}; -``` - -#### Nesting `switch` expressions - -The `switch` introduction functionality works with nested `switch` expressions: - -##### Before ``` -let foo (arg: list 'a) => switch arg { - | [] => failwith "" - | [_, ...xs] => xs -}; -``` - -##### After -``` -let foo (arg: list 'a) => switch arg { - | [] => failwith "" - | [_, ...xs] => switch xs { - | [] => failwith "" - | [_, ..._] => failwith "" - } -}; -``` - -#### Splitting a pattern without introducing a `switch` - -The case split feature can be used to split an existing pattern further: - -##### Before -``` -let foo (arg: list 'a) => switch arg { - | [] => failwith "" - | [x, ...xs] => failwith "" -}; -``` - -##### After -``` -let foo (arg: list 'a) => switch arg { - | [] => failwith "" - | [_] | [_, _, ..._] => failwith "" -}; +"reason.codelens.enabled": true ``` +```` diff --git a/package.json b/package.json index 2c14199..c338580 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "name": "reasonml", "displayName": "OCaml and Reason IDE", "description": "OCaml and Reason language support", - "version": "1.0.38", + "version": "1.1.0", "publisher": "freebroccolo", "license": "Apache-2.0", "bugs": { @@ -16,143 +16,70 @@ "engines": { "vscode": "^1.21.0" }, - "categories": ["Formatters", "Programming Languages", "Linters", "Snippets"], - "keywords": ["ocaml", "reason", "bucklescript", "reasonml", "merlin"], + "categories": [ + "Formatters", + "Programming Languages", + "Linters", + "Snippets" + ], + "keywords": [ + "ocaml", + "reason", + "bucklescript", + "reasonml", + "merlin" + ], "icon": "assets/logo.png", - "activationEvents": ["onLanguage:ocaml", "onLanguage:reason"], + "activationEvents": [ + "onLanguage:ocaml", + "onLanguage:reason" + ], "main": "./out/src/extension", "contributes": { - "commands": [ - { - "command": "reason.caseSplit", - "title": "Reason: Case Split" - }, - { - "command": "reason.showMerlinFiles", - "title": "Reason: Show Merlin Files" + "commands": [{ + "command": "reason.restart", + "title": "VSCode Reason: Restart Merlin Language Server" }, { - "command": "reason.showAvailableLibraries", - "title": "Reason: Show Libraries Available via Dependencies" - }, - { - "command": "reason.showProjectEnv", - "title": "Reason: Show Environment" + "command": "reason.init", + "title": "VSCode Reason: Initialize" } ], "configuration": { "type": "object", "title": "Reason configuration", "properties": { - "reason.codelens.unicode": { - "type": "boolean", - "default": true, - "description": "Enable the use of unicode symbols in codelens." - }, - "reason.codelens.enabled": { - "type": "boolean", - "default": false, - "description": "Specifies whether the code lens is shown." - }, - "reason.debounce.linter": { - "oneOf": [ - { - "type": "integer" - }, - { - "enum": ["Infinity"] - } - ], - "default": 500, - "description": - "How long to idle (in milliseconds) after keypresses before refreshing linter diagnostics. Smaller values refresh diagnostics more quickly." - }, - "reason.diagnostics.tools": { - "type": "array", - "items": { - "enum": ["merlin", "bsb"] - }, - "default": ["merlin"], - "maxItems": 2, - "uniqueItems": true, - "description": - "Specifies which tool or tools will be used to get diagnostics. If you choose both \"merlin\" and \"bsb\", merlin will be used while editing and bsb when saving." - }, "reason.format.width": { - "type": ["number", null], + "type": [ + "number", + null + ], "default": null, "description": "Set the width of lines when formatting code with refmt" }, - "reason.path.bsb": { - "type": "string", - "default": "./node_modules/bs-platform/lib/bsb.exe", - "description": "The path to the `bsb` binary." - }, - "reason.path.ocamlfind": { - "type": "string", - "default": "ocamlfind", - "description": "The path to the `ocamlfind` binary." - }, - "reason.path.esy": { - "type": "string", - "default": "esy", - "description": "The path to the `esy` binary." - }, - "reason.path.env": { - "type": "string", - "default": "env", - "description": - "The path to the `env` command which prints the language server environment for debugging editor issues." - }, - "reason.path.ocamlmerlin": { - "type": "string", - "default": "ocamlmerlin", - "description": "The path to the `ocamlmerlin` binary." - }, - "reason.path.ocpindent": { + "reason.path.ocamlmerlin-lsp": { "type": "string", - "default": "ocp-indent", - "description": "The path to the `ocp-indent` binary." + "default": null, + "description": "The path to the `ocamlmerlin-lsp` binary." }, - "reason.path.opam": { + "reason.path.ocamlmerlin-reason": { "type": "string", - "default": "opam", - "description": "The path to the `opam` binary." + "default": null, + "description": "The path to the `ocamlmerlin-reason` binary." }, - "reason.path.rebuild": { + "reason.path.ocamlformat": { "type": "string", - "default": "rebuild", - "description": "The path to the `rebuild` binary." + "default": "ocamlformat", + "description": "The path to the `ocamlformat` binary." }, "reason.path.refmt": { "type": "string", "default": "refmt", "description": "The path to the `refmt` binary." - }, - "reason.path.refmterr": { - "type": "string", - "default": "refmterr", - "description": "The path to the `refmterr` binary." - }, - "reason.path.rtop": { - "type": "string", - "default": "rtop", - "description": "The path to the `rtop` binary." - }, - "reason.server.languages": { - "type": "array", - "items": { - "enum": ["ocaml", "reason"] - }, - "default": ["ocaml", "reason"], - "maxItems": 2, - "uniqueItems": true, - "description": "The list of languages enable support for in the language server." } } }, - "grammars": [ - { + "grammars": [{ "language": "ocaml", "scopeName": "source.ocaml", "path": "./syntaxes/ocaml.json" @@ -205,7 +132,9 @@ { "scopeName": "markdown.reason.codeblock", "path": "./syntaxes/reason-markdown-codeblock.json", - "injectTo": ["text.html.markdown"], + "injectTo": [ + "text.html.markdown" + ], "embeddedLanguages": { "meta.embedded.block.reason": "reason" } @@ -213,17 +142,23 @@ { "scopeName": "markdown.ocaml.codeblock", "path": "./syntaxes/ocaml-markdown-codeblock.json", - "injectTo": ["text.html.markdown"], + "injectTo": [ + "text.html.markdown" + ], "embeddedLanguages": { "meta.embedded.block.ocaml": "ocaml" } } ], - "languages": [ - { + "languages": [{ "id": "ocaml", - "aliases": ["OCaml"], - "extensions": [".ml", ".mli"], + "aliases": [ + "OCaml" + ], + "extensions": [ + ".ml", + ".mli" + ], "configuration": "./ocaml.configuration.json" }, { @@ -234,23 +169,40 @@ }, { "id": "ocaml.merlin", - "aliases": ["Merlin"], - "extensions": ["merlin"] + "aliases": [ + "Merlin" + ], + "extensions": [ + "merlin" + ] }, { "id": "ocaml.ocamlbuild", - "aliases": ["OCamlbuild"], - "extensions": ["_tags"] + "aliases": [ + "OCamlbuild" + ], + "extensions": [ + "_tags" + ] }, { "id": "ocaml.opam", - "aliases": ["OPAM"], - "extensions": ["opam"] + "aliases": [ + "OPAM" + ], + "extensions": [ + "opam" + ] }, { "id": "reason", - "aliases": ["Reason"], - "extensions": [".re", ".rei"], + "aliases": [ + "Reason" + ], + "extensions": [ + ".re", + ".rei" + ], "configuration": "./reason.configuration.json" }, { @@ -263,50 +215,37 @@ "id": "reason.hover.type" } ], - "menus": { - "editor/context": [ + "snippets": [{ + "language": "reason", + "path": "./snippets/reason.json" + }], + "problemMatchers": [{ + "name": "ocamlc", + "fileLocation": [ + "relative", + "${workspaceFolder}" + ], + "pattern": [{ + "regexp": "^\\s*\\bFile\\b\\s*\"(.*)\",\\s*\\bline\\b\\s*(\\d+),\\s*\\bcharacters\\b\\s*(\\d+)-(\\d+)\\s*:\\s*$", + "file": 1, + "line": 2, + "column": 3, + "endColumn": 4 + }, { - "command": "reason.caseSplit", - "group": "reason", - "when": "editorTextFocus && resourceLangId == reason" + "regexp": "^(?:\\s*\\bParse\\b\\s*)?\\s*\\b([Ee]rror|Warning)\\b\\s*(?:\\(\\s*\\bwarning\\b\\s*(\\d+)\\))?\\s*:\\s*(.*)$", + "severity": 1, + "code": 2, + "message": 3 } ] - }, - "snippets": [ - { - "language": "reason", - "path": "./snippets/reason.json" - } - ], - "problemMatchers": [ - { - "name": "ocamlc", - - "fileLocation": ["relative", "${workspaceFolder}"], - "pattern": [ - { - "regexp": - "^\\s*\\bFile\\b\\s*\"(.*)\",\\s*\\bline\\b\\s*(\\d+),\\s*\\bcharacters\\b\\s*(\\d+)-(\\d+)\\s*:\\s*$", - "file": 1, - "line": 2, - "column": 3, - "endColumn": 4 - }, - { - "regexp": - "^(?:\\s*\\bParse\\b\\s*)?\\s*\\b([Ee]rror|Warning)\\b\\s*(?:\\(\\s*\\bwarning\\b\\s*(\\d+)\\))?\\s*:\\s*(.*)$", - "severity": 1, - "code": 2, - "message": 3 - } - ] - } - ] + }] }, "scripts": { "build": "tsc -p ./ && node script/syntax.js", + "watch": "tsc -p ./ -w", "format": "./node_modules/.bin/prettier --write \"src/**/*.ts\"", - "lint": "tslint --project tsconfig.json", + "lint": "true", "postinstall": "node ./node_modules/vscode/bin/install", "prebuild": "npm run format && npm run lint", "vscode:prepublish": "node script/syntax.js" @@ -315,15 +254,18 @@ "@types/lodash.flatmap": "^4.5.3", "@types/node": "9.6.2", "@types/pegjs": "0.10.0", + "@types/uuid": "^3.4.4", "prettier": "1.11.1", "tslint": "5.9.1", "typescript": "2.8.1", "vscode": "1.1.14" }, "dependencies": { + "@types/semver": "^6.0.0", "lodash.flatmap": "^4.5.0", - "ocaml-language-server": "1.0.35", "pegjs": "0.10.0", + "semver": "^6.0.0", + "uuid": "^3.3.2", "vscode-jsonrpc": "3.6.0", "vscode-languageclient": "4.0.1", "vscode-languageserver": "4.0.0", diff --git a/src/client/command/doShowAvailableLibraries.ts b/src/client/command/doShowAvailableLibraries.ts deleted file mode 100644 index da9a1ba..0000000 --- a/src/client/command/doShowAvailableLibraries.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { remote } from "ocaml-language-server"; -import * as vscode from "vscode"; -import * as client from "vscode-languageclient"; -import * as LSP from "vscode-languageserver-protocol"; - -export function register(context: vscode.ExtensionContext, languageClient: client.LanguageClient): void { - context.subscriptions.push( - vscode.commands.registerTextEditorCommand("reason.showAvailableLibraries", async editor => { - const docURI: LSP.TextDocumentIdentifier = { - uri: editor.document.uri.toString(), - }; - const libraryLines = languageClient.sendRequest(remote.server.giveAvailableLibraries, docURI); - await vscode.window.showQuickPick(libraryLines); - return; - }), - ); -} diff --git a/src/client/command/doShowMerlinFiles.ts b/src/client/command/doShowMerlinFiles.ts deleted file mode 100644 index 7fb110a..0000000 --- a/src/client/command/doShowMerlinFiles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { remote } from "ocaml-language-server"; -import * as vscode from "vscode"; -import * as client from "vscode-languageclient"; -import * as LSP from "vscode-languageserver-protocol"; - -export function register(context: vscode.ExtensionContext, languageClient: client.LanguageClient): void { - context.subscriptions.push( - vscode.commands.registerTextEditorCommand("reason.showMerlinFiles", async editor => { - const docURI: LSP.TextDocumentIdentifier = { - uri: editor.document.uri.toString(), - }; - const merlinFiles: string[] = await languageClient.sendRequest(remote.server.giveMerlinFiles, docURI); - const selected: string | undefined = await vscode.window.showQuickPick(merlinFiles); - if (null == selected) return; - const textDocument = await vscode.workspace.openTextDocument(selected); - await vscode.window.showTextDocument(textDocument); - }), - ); -} diff --git a/src/client/command/doShowProjectEnv.ts b/src/client/command/doShowProjectEnv.ts deleted file mode 100644 index f318d2c..0000000 --- a/src/client/command/doShowProjectEnv.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { remote } from "ocaml-language-server"; -import * as vscode from "vscode"; -import * as client from "vscode-languageclient"; -import * as LSP from "vscode-languageserver-protocol"; - -const SHOW_ALL_STR = "Show Entire Environment"; -export function register(context: vscode.ExtensionContext, languageClient: client.LanguageClient): void { - context.subscriptions.push( - vscode.commands.registerTextEditorCommand("reason.showProjectEnv", async editor => { - const docURI: LSP.TextDocumentIdentifier = { - uri: editor.document.uri.toString(), - }; - const projectEnv: string[] = await languageClient.sendRequest(remote.server.giveProjectEnv, docURI); - const projectEnvWithAll = [SHOW_ALL_STR].concat(projectEnv); - const selected = await vscode.window.showQuickPick(projectEnvWithAll); - if (null == selected) return; - const content = selected === SHOW_ALL_STR ? projectEnv.join("\n") : selected; - const textDocument = await vscode.workspace.openTextDocument({ - content, - language: "shellscript", - }); - await vscode.window.showTextDocument(textDocument); - }), - ); -} diff --git a/src/client/command/doSplitCases.ts b/src/client/command/doSplitCases.ts deleted file mode 100644 index 93032d4..0000000 --- a/src/client/command/doSplitCases.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { merlin, remote } from "ocaml-language-server"; -import * as vscode from "vscode"; -import * as client from "vscode-languageclient"; -import * as LSP from "vscode-languageserver-protocol"; - -async function execute(editor: vscode.TextEditor, destruct: merlin.Case.Destruct): Promise { - const [{ end, start }, content] = destruct; - return editor.edit(editBuilder => { - const range = new vscode.Range( - new vscode.Position(start.line - 1, start.col), - new vscode.Position(end.line - 1, end.col), - ); - const cases = format(editor, content); - editBuilder.replace(range, cases); - }); -} - -export function format(editor: vscode.TextEditor, content: string): string { - const line = editor.document.lineAt(editor.selection.start); - const match = line.text.match(/^\s*/); - const indentation = match && match.length > 0 ? match[0] : ""; // FIXME: use use indentation settings - let result = content; - result = format.deleteWhitespace(result); - result = format.deleteParentheses(result); - result = format.indentExpression(indentation, result); - result = format.indentPatterns(result); - result = format.insertPlaceholders(result); - return result; -} - -export namespace format { - export function deleteParentheses(content: string): string { - return content.replace(/^\(|\n\)$/g, ""); - } - export function deleteWhitespace(content: string): string { - return content.replace(/\n$/, ""); - } - export function indentExpression(indentation: string, content: string): string { - return !/^\bswitch\b/g.test(content) - ? content - : content.replace(/\|/g, `${indentation}|`).replace(/}$/g, `${indentation}}`); - } - export function indentPatterns(content: string): string { - return content.replace(/{(?!\s)/g, "{ ").replace(/([^\s])}/g, "$1 }"); - } - export function insertPlaceholders(content: string): string { - return content.replace(/\(\?\?\)/g, `failwith ""`); - } -} - -export function register(context: vscode.ExtensionContext, languageClient: client.LanguageClient): void { - // FIXME: using the edit builder passed in to the command doesn't seem to work - context.subscriptions.push( - vscode.commands.registerTextEditorCommand("reason.caseSplit", async (editor): Promise => { - const textDocument = { uri: editor.document.uri.toString() }; - const rangeCode = editor.document.getWordRangeAtPosition(editor.selection.start); - if (null == rangeCode) return; - const range = LSP.Range.create(rangeCode.start, rangeCode.end); - const params = { range, textDocument }; - try { - const response = await languageClient.sendRequest(remote.server.giveCaseAnalysis, params); - if (null != response) await execute(editor, response); - } catch (err) { - // FIXME: clean this up - // vscode.window.showErrorMessage(JSON.stringify(err)); - const pattern = /Destruct not allowed on non-immediate type/; - if (pattern.test(err)) { - vscode.window.showWarningMessage( - "More type info needed for case split; try adding an annotation somewhere, e.g., (pattern: type).", - ); - } - } - }), - ); -} diff --git a/src/client/command/index.ts b/src/client/command/index.ts index cfc38a2..9a8c956 100644 --- a/src/client/command/index.ts +++ b/src/client/command/index.ts @@ -1,18 +1,10 @@ import * as vscode from "vscode"; import * as client from "vscode-languageclient"; -import * as doShowAvailableLibraries from "./doShowAvailableLibraries"; -import * as doShowMerlinFiles from "./doShowMerlinFiles"; -import * as doShowProjectEnv from "./doShowProjectEnv"; -import * as doSplitCases from "./doSplitCases"; import * as fixEqualsShouldBeArrow from "./fixEqualsShouldBeArrow"; import * as fixMissingSemicolon from "./fixMissingSemicolon"; import * as fixUnusedVariable from "./fixUnusedVariable"; export function registerAll(context: vscode.ExtensionContext, languageClient: client.LanguageClient): void { - doShowMerlinFiles.register(context, languageClient); - doShowProjectEnv.register(context, languageClient); - doShowAvailableLibraries.register(context, languageClient); - doSplitCases.register(context, languageClient); fixEqualsShouldBeArrow.register(context, languageClient); fixMissingSemicolon.register(context, languageClient); fixUnusedVariable.register(context, languageClient); diff --git a/src/client/index.ts b/src/client/index.ts index 2a44e90..da1812d 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -29,29 +29,53 @@ class ErrorHandler { } export async function launch(context: vscode.ExtensionContext): Promise { - const reasonConfig = vscode.workspace.getConfiguration("reason"); - const module = context.asAbsolutePath(path.join("node_modules", "ocaml-language-server", "bin", "server")); - const options = { execArgv: ["--nolazy", "--inspect=6009"] }; - const transport = client.TransportKind.ipc; - const run = { module, transport }; - const debug = { - module, - options, - transport, + return launchMerlinLsp(context); +} + +function getMerlinLspOptions(lsp: string, reason: string | undefined) { + let envPath = reason ? `${path.dirname(reason)}:${process.env.PATH}` : process.env.PATH; + let run = { + args: [], + command: lsp, + options: { + env: { + ...process.env, + MERLIN_LOG: "-", + OCAMLFIND_CONF: "/dev/null", + OCAMLRUNPARAM: "b", + PATH: envPath, + }, + }, }; - const serverOptions = { run, debug }; + + return { + debug: run, + run: run, + }; +} + +export async function launchMerlinLsp(context: vscode.ExtensionContext): Promise { + const reasonConfig = vscode.workspace.getConfiguration("reason"); + const lsp = reasonConfig.get(`path.ocamlmerlin-lsp`); + const reason = reasonConfig.get(`path.ocamlmerlin-reason`); + + if (!lsp) { + vscode.window.showInformationMessage("reason.path.ocamlmerlin-lsp is not specified"); + return; + } + + const serverOptions = getMerlinLspOptions(lsp, reason); const languages = reasonConfig.get("server.languages", ["ocaml", "reason"]); const documentSelector = flatMap(languages, (language: string) => [ { language, scheme: "file" }, { language, scheme: "untitled" }, ]); - const clientOptions: client.LanguageClientOptions = { - diagnosticCollectionName: "ocaml-language-server", + diagnosticCollectionName: "ocamlmerlin-lsp", documentSelector, errorHandler: new ErrorHandler(), initializationOptions: reasonConfig, - outputChannelName: "OCaml Language Server", + outputChannelName: "Merlin Language Server", stdioEncoding: "utf8", synchronize: { configurationSection: "reason", diff --git a/src/extension.ts b/src/extension.ts index 8b81e74..bc19a29 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,98 +1,21 @@ // tslint:disable object-literal-sort-keys - import * as vscode from "vscode"; import * as client from "./client"; - -const reasonConfiguration = { - indentationRules: { - decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/, - increaseIndentPattern: /^.*\{[^}"']*$/, - }, - onEnterRules: [ - { - beforeText: /^.*\b(switch|try)\b[^\{]*{\s*$/, - action: { - indentAction: vscode.IndentAction.IndentOutdent, - appendText: "| ", - }, - }, - { - beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, - afterText: /^\s*\*\/$/, - action: { - indentAction: vscode.IndentAction.IndentOutdent, - appendText: " * ", - }, - }, - { - beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, - action: { - indentAction: vscode.IndentAction.None, - appendText: " * ", - }, - }, - { - beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/, - action: { - indentAction: vscode.IndentAction.None, - appendText: "* ", - }, - }, - { - beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/, - action: { - indentAction: vscode.IndentAction.None, - removeText: 1, - }, - }, - { - beforeText: /^(\t|(\ \ ))*\ \*[^/]*\*\/\s*$/, - action: { - indentAction: vscode.IndentAction.None, - removeText: 1, - }, - }, - { - beforeText: /^.*\bfun\b\s*$/, - action: { - indentAction: vscode.IndentAction.None, - appendText: "| ", - }, - }, - { - beforeText: /^\s*\btype\b.*=(.*[^;\\{<]\s*)?$/, - afterText: /^\s*$/, - action: { - indentAction: vscode.IndentAction.None, - appendText: " | ", - }, - }, - { - beforeText: /^(\t|[ ]{2})*[\|]([^!$%&*+-/<=>?@^~;}])*(?:$|=>.*[^\s\{]\s*$)/m, - action: { - indentAction: vscode.IndentAction.None, - appendText: "| ", - }, - }, - { - beforeText: /^(\t|(\ \ ))*\|(.*[;])$/, - action: { - indentAction: vscode.IndentAction.Outdent, - }, - }, - { - beforeText: /^(\t|(\ \ ))*;\s*$/, - action: { - indentAction: vscode.IndentAction.Outdent, - }, - }, - ], - wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\"\,\.\<\>\/\?\s]+)/g, -}; +import { register as registerOcamlForamtter } from "./formatters/ocaml"; +import { register as registerReasonForamtter } from "./formatters/reason"; +import reasonConfiguration from "./reasonConfiguration"; export async function activate(context: vscode.ExtensionContext) { + function start() { + client.launch(context); + } + context.subscriptions.push(vscode.languages.setLanguageConfiguration("reason", reasonConfiguration)); - await client.launch(context); + registerOcamlForamtter(); + registerReasonForamtter(); + + vscode.commands.registerCommand("reason.restart", start); + start(); } export function deactivate() { diff --git a/src/formatters/ocaml.ts b/src/formatters/ocaml.ts new file mode 100644 index 0000000..24e45a5 --- /dev/null +++ b/src/formatters/ocaml.ts @@ -0,0 +1,43 @@ +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { v4 as uuidv4 } from "uuid"; +import * as vscode from "vscode"; +import { getFormatter, getFullTextRange } from "../utils"; + +export function register() { + const configuration = vscode.workspace.getConfiguration("reason"); + const rootPath = vscode.workspace.rootPath || ""; + + vscode.languages.registerDocumentFormattingEditProvider( + { scheme: "file", language: "ocaml" }, + { + async provideDocumentFormattingEdits(_document: vscode.TextDocument): Promise { + const textEditor = vscode.window.activeTextEditor; + const formatter = await getFormatter(configuration, "ocamlformat"); + + if (!formatter) return []; + + if (textEditor) { + const tempFileName = path.join(os.tmpdir(), `vscode-reasonml-ocamlformat-${uuidv4()}.ml`); + fs.writeFileSync(tempFileName, textEditor.document.getText(), "utf8"); + try { + const filePath = textEditor.document.fileName; + const formattedText = execSync( + `cd ${rootPath} && ${formatter} --name=${filePath} ${tempFileName}`, + ).toString(); + const textRange = getFullTextRange(textEditor); + fs.unlinkSync(tempFileName); + return [vscode.TextEdit.replace(textRange, formattedText)]; + } catch (e) { + fs.unlinkSync(tempFileName); + return []; + } + } else { + return []; + } + }, + }, + ); +} diff --git a/src/formatters/reason.ts b/src/formatters/reason.ts new file mode 100644 index 0000000..bf22efc --- /dev/null +++ b/src/formatters/reason.ts @@ -0,0 +1,39 @@ +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { v4 as uuidv4 } from "uuid"; +import * as vscode from "vscode"; +import { getFormatter, getFullTextRange } from "../utils"; + +export function register() { + const configuration = vscode.workspace.getConfiguration("reason"); + + vscode.languages.registerDocumentFormattingEditProvider( + { scheme: "file", language: "reason" }, + { + async provideDocumentFormattingEdits(_document: vscode.TextDocument): Promise { + const textEditor = vscode.window.activeTextEditor; + const formatter = await getFormatter(configuration, "refmt"); + + if (!formatter) return []; + + if (textEditor) { + const tempFileName = path.join(os.tmpdir(), `vscode-reasonml-refmt-${uuidv4()}.re`); + fs.writeFileSync(tempFileName, textEditor.document.getText(), "utf8"); + try { + const formattedText = execSync(`${formatter} ${tempFileName}`).toString(); + const textRange = getFullTextRange(textEditor); + fs.unlinkSync(tempFileName); + return [vscode.TextEdit.replace(textRange, formattedText)]; + } catch (e) { + fs.unlinkSync(tempFileName); + return []; + } + } else { + return []; + } + }, + }, + ); +} diff --git a/src/reasonConfiguration.ts b/src/reasonConfiguration.ts new file mode 100644 index 0000000..df30684 --- /dev/null +++ b/src/reasonConfiguration.ts @@ -0,0 +1,88 @@ +import * as vscode from "vscode"; + +export default { + indentationRules: { + decreaseIndentPattern: /^(.*\*\/)?\s*\}.*$/, + increaseIndentPattern: /^.*\{[^}"']*$/, + }, + onEnterRules: [ + { + beforeText: /^.*\b(switch|try)\b[^\{]*{\s*$/, + action: { + indentAction: vscode.IndentAction.IndentOutdent, + appendText: "| ", + }, + }, + { + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + afterText: /^\s*\*\/$/, + action: { + indentAction: vscode.IndentAction.IndentOutdent, + appendText: " * ", + }, + }, + { + beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, + action: { + indentAction: vscode.IndentAction.None, + appendText: " * ", + }, + }, + { + beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/, + action: { + indentAction: vscode.IndentAction.None, + appendText: "* ", + }, + }, + { + beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/, + action: { + indentAction: vscode.IndentAction.None, + removeText: 1, + }, + }, + { + beforeText: /^(\t|(\ \ ))*\ \*[^/]*\*\/\s*$/, + action: { + indentAction: vscode.IndentAction.None, + removeText: 1, + }, + }, + { + beforeText: /^.*\bfun\b\s*$/, + action: { + indentAction: vscode.IndentAction.None, + appendText: "| ", + }, + }, + { + beforeText: /^\s*\btype\b.*=(.*[^;\\{<]\s*)?$/, + afterText: /^\s*$/, + action: { + indentAction: vscode.IndentAction.None, + appendText: " | ", + }, + }, + { + beforeText: /^(\t|[ ]{2})*[\|]([^!$%&*+-/<=>?@^~;}])*(?:$|=>.*[^\s\{]\s*$)/m, + action: { + indentAction: vscode.IndentAction.None, + appendText: "| ", + }, + }, + { + beforeText: /^(\t|(\ \ ))*\|(.*[;])$/, + action: { + indentAction: vscode.IndentAction.Outdent, + }, + }, + { + beforeText: /^(\t|(\ \ ))*;\s*$/, + action: { + indentAction: vscode.IndentAction.Outdent, + }, + }, + ], + wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\"\,\.\<\>\/\?\s]+)/g, +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..9a529b9 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,39 @@ +import { execSync } from "child_process"; +import * as path from "path"; +import * as vscode from "vscode"; + +export function getFullTextRange(textEditor: vscode.TextEditor) { + const firstLine = textEditor.document.lineAt(0); + const lastLine = textEditor.document.lineAt(textEditor.document.lineCount - 1); + + return new vscode.Range( + 0, + firstLine.range.start.character, + textEditor.document.lineCount - 1, + lastLine.range.end.character, + ); +} + +function getExecutablePath(executable: string) { + try { + return execSync(`which ${executable}`).toString(); + } catch (_e) { + return null; + } +} + +export async function getFormatter(configuration: vscode.WorkspaceConfiguration, formatterName: string) { + const rootPath = vscode.workspace.rootPath || ""; + const formatterPath = configuration.get(`path.${formatterName}`) || formatterName; + + const formatter = + formatterPath === formatterName ? getExecutablePath(formatterName) : path.resolve(rootPath, formatterPath); + + if (!formatter) { + vscode.window.showInformationMessage( + `${formatterPath} is not available. Please specify "reason.path.${formatterName}"`, + ); + } + + return formatter; +} diff --git a/vscode-reasonml.code-workspace b/vscode-reasonml.code-workspace index 08a9101..c0d6e19 100644 --- a/vscode-reasonml.code-workspace +++ b/vscode-reasonml.code-workspace @@ -1,12 +1,7 @@ { - "folders": [ - { - "path": "../ocaml-language-server" - }, - { - "path": "." - } - ], + "folders": [{ + "path": "." + }], "settings": { "typescript.tsdk": "./node_modules/typescript/lib" } diff --git a/yarn.lock b/yarn.lock index d28a8ab..37f9764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,11 @@ version "4.14.109" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.109.tgz#b1c4442239730bf35cabaf493c772b18c045886d" +"@types/node@*": + version "11.9.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.6.tgz#c632bbcc780a1349673a6e2e9b9dfa8c369d3c74" + integrity sha512-4hS2K4gwo9/aXIcoYxCtHpdgd8XUeDmo1siRCAH3RziXB65JlPqUFMtfy9VPj+og7dp3w1TFjGwYga4e0m9GwA== + "@types/node@9.6.2": version "9.6.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.2.tgz#e49ac1adb458835e95ca6487bc20f916b37aff23" @@ -20,6 +25,18 @@ version "0.10.0" resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.0.tgz#a14736222e6208e1c68c0be231ab1c53ddc55392" +"@types/semver@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.0.0.tgz#86ba89f02a414e39c68d02b351872e4ed31bd773" + integrity sha512-OO0srjOGH99a4LUN2its3+r6CBYcplhJ466yLqs+zvAWgphCpS8hYZEZ797tRDP/QKcqTdb/YCN6ifASoAWkrQ== + +"@types/uuid@^3.4.4": + version "3.4.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5" + integrity sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw== + dependencies: + "@types/node" "*" + ajv@^5.1.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" @@ -130,12 +147,6 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" -async@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" - dependencies: - lodash "^4.14.0" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -361,10 +372,6 @@ deep-assign@^1.0.0: dependencies: is-obj "^1.0.0" -deepmerge@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -1126,14 +1133,6 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" -lodash@4.17.5, lodash@^4.14.0: - version "4.17.5" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" - -lokijs@1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.3.tgz#6952722ffa3049a55a5e1c10ee4a0947a3e5e19b" - map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" @@ -1257,22 +1256,6 @@ object.omit@^2.0.0: for-own "^0.1.4" is-extendable "^0.1.1" -ocaml-language-server@1.0.35: - version "1.0.35" - resolved "https://registry.yarnpkg.com/ocaml-language-server/-/ocaml-language-server-1.0.35.tgz#e80dab2d40a73b0eb629efc724a7b98581f801d1" - dependencies: - async "2.6.0" - deepmerge "2.1.0" - glob "7.1.2" - lodash "4.17.5" - lokijs "1.5.3" - pegjs "0.10.0" - vscode-jsonrpc "3.6.0" - vscode-languageclient "4.0.1" - vscode-languageserver "4.0.0" - vscode-languageserver-protocol "3.6.0" - vscode-uri "1.0.3" - once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -1524,6 +1507,11 @@ semver@^5.3.0, semver@^5.4.1: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" +semver@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" + integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" @@ -1763,6 +1751,11 @@ uuid@^3.0.0, uuid@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" +uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + vali-date@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6" @@ -1878,7 +1871,7 @@ vscode-languageserver@4.0.0: vscode-languageserver-protocol "^3.6.0" vscode-uri "^1.0.1" -vscode-uri@1.0.3, vscode-uri@^1.0.1: +vscode-uri@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.3.tgz#631bdbf716dccab0e65291a8dc25c23232085a52"