diff --git a/.gitignore b/.gitignore index 9722207..6219f32 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ .bsb.lock lib/bs -/node_modules +node_modules test/**/*.js doc/tags @@ -37,3 +37,8 @@ doc/tags !rescript-vscode/extension/server/win32/rescript-editor-support.exe rescript-vscode/extension/server/node_modules/.bin/ + +examples/**/node_modules +examples/**/lib +examples/**/src/*.js +examples/**/.merlin diff --git a/CHANGELOG.md b/CHANGELOG.md index 670ef53..44cebbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,18 @@ ## master +** Improvements: ** + +- Upgrade to `rescript-vscode@1.4.0` (see changes [here](https://github.com/rescript-lang/rescript-vscode/blob/1.0.4/HISTORY.md#104)) +- Add proper monorepo support (`e.g. yarn workspaces`) + - Detects `bsb` / `bsc` correctly for each buffer separately. + - Heuristic for detecting the binaries: For the current file, find the nearest `node_modules/bs-platform` folder for the binaries + - Adds an `augroup RescriptAutoProjectEnv` that sets the environment on every `.res` / `.resi` related read / write / new file event + - Will also update the environment on each `format` and `build` call to make it sync up for all non-rescript buffers + - On each env update, it updates the local working directory to the updated project root path as well - Fixes issue with long template strings breaking the syntax highlighting +- Fixes an issue where `:RescriptBuild` would fail in non-rescript buffers due to a wrongly scoped script variable (was buffer only) +- Add new commands `:RescriptBuildWorld` and `:RescriptCleanWorld` for cleaning / building all sources + dependencies ## 1.1.0 diff --git a/README.md b/README.md index d51ef85..37384bb 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - Filetype detection for `.res`, `.resi` - Basic automatic indentation - Includes LSP for coc-vim usage +- Proper tooling detection for monorepo like setups (yarn workspaces) **Provided by vim-rescript commands:** - Formatting `.res` files w/ syntax error diagnostics in VIM quickfix @@ -24,6 +25,12 @@ - Building the current projec w/ build diagnostics in VIM quickfix - Autocompletion w/ Vim's omnicomplete +**Monorepo support:** + +The vim-rescript plugin automatically updates its project environment on each file open separately. +- Tested for yarn workspaces (see [./examples/monorepo-yarn-workspaces]) +- **Note for non-LSP usage:** Always make sure to switch to a `.res` file **within the project you want to compile** before running `:RescriptBuild` etc. + See `:h rescript` for the detailed [helpfile](./doc/rescript.txt). ## Requirements diff --git a/autoload/rescript.vim b/autoload/rescript.vim index 0e9679e..14ce88a 100644 --- a/autoload/rescript.vim +++ b/autoload/rescript.vim @@ -21,18 +21,12 @@ function! s:ShowInPreview(fname, fileType, lines) endif endfunction -" Inits the plugin variables, e.g. finding all the necessary binaries -function! rescript#Init() - if has('macunix') - let b:rescript_arch = "darwin" - elseif has('win32') - let b:rescript_arch = "win32" - elseif has('unix') - let b:rescript_arch = "linux" - endif - +" Configures the project related globals based on the current buffer location +" This is needed for supporting monorepos with multiple projects / multiple +" bs-platform setups +function! rescript#UpdateProjectEnv() " Looks for the nearest node_modules directory - let l:res_bin_dir = finddir('node_modules', ".;") . "/bs-platform/" . b:rescript_arch + let l:res_bin_dir = finddir('node_modules/bs-platform/', ".;") . s:rescript_arch "if !exists("g:rescript_compile_exe") let g:rescript_compile_exe = l:res_bin_dir . "/bsc.exe" @@ -42,17 +36,9 @@ function! rescript#Init() let g:rescript_build_exe = l:res_bin_dir . "/bsb.exe" "endif - " Needed for state tracking of the formatting error state - let s:got_format_err = 0 - let s:got_build_err = 0 - - if !exists("g:rescript_editor_support_exe") - let g:rescript_editor_support_exe = s:rescript_plugin_dir . "/rescript-vscode/extension/server/" . b:rescript_arch . "/rescript-editor-support.exe" - endif - - " Not sure why, but adding a ".;" doesn't find bsconfig when - " the editor was started without a specific file within the project - let g:rescript_project_config = findfile("bsconfig.json") + " Note that this doesn't find bsconfig when the editor was started without a + " specific file within the project + let g:rescript_project_config = findfile("bsconfig.json", ".;") " Try to find the nearest .git folder instead if g:rescript_project_config == "" @@ -60,6 +46,31 @@ function! rescript#Init() else let g:rescript_project_root = fnamemodify(g:rescript_project_config, ":p:h") endif + + " Make sure that our local working directory is in the rescript_project_root + exe "lcd " . g:rescript_project_root +endfunction + +" Inits the plugin variables, e.g. finding all the plugin related binaries +" and initialising some internal state for UI (error window etc.) +function! rescript#Init() + if has('macunix') + let s:rescript_arch = "darwin" + elseif has('win32') + let s:rescript_arch = "win32" + elseif has('unix') + let s:rescript_arch = "linux" + endif + + " Needed for state tracking of the formatting error state + let s:got_format_err = 0 + let s:got_build_err = 0 + + if !exists("g:rescript_editor_support_exe") + let g:rescript_editor_support_exe = s:rescript_plugin_dir . "/rescript-vscode/extension/server/" . s:rescript_arch . "/rescript-editor-support.exe" + endif + + call rescript#UpdateProjectEnv() endfunction function! s:DeleteLines(start, end) abort @@ -79,6 +90,7 @@ function! rescript#GetRescriptVscodeVersion() endfunction function! rescript#DetectVersion() + call rescript#UpdateProjectEnv() let l:command = g:rescript_compile_exe . " -version" silent let l:output = system(l:command) @@ -96,6 +108,8 @@ function! rescript#DetectVersion() endfunction function! rescript#Format() + call rescript#UpdateProjectEnv() + let l:ext = expand("%:e") if matchstr(l:ext, 'resi\?') == "" @@ -376,8 +390,15 @@ function! rescript#Complete(findstart, base) return l:ret endfunction -function! rescript#BuildProject() - let out = system(g:rescript_build_exe) +function! rescript#BuildProject(...) + call rescript#UpdateProjectEnv() + + let l:cmd = g:rescript_build_exe + if a:0 ==? 1 + let l:cmd = g:rescript_build_exe . " " . a:1 + endif + + let out = system(l:cmd) " We don't rely too heavily on exit codes. If there's a compiler.log, " then there is either an error or a warning, so we rely on the existence diff --git a/doc/rescript.txt b/doc/rescript.txt index 82c1da3..a432993 100644 --- a/doc/rescript.txt +++ b/doc/rescript.txt @@ -118,7 +118,16 @@ COMMANDS *rescript-commands* after writing your new .res file, otherwise the compiler will not compile. *:RescriptBuild* - Builds your current project with g:rescript_build_exe + Builds your current project with g:rescript_build_exe (without -make-world + flag) + +*:RescriptBuildWorld* + Builds your current project with g:rescript_build_exe (with -make-world). + This is useful for building your ReScript dependencies as well. + +*:RescriptCleanWorld* + Cleans all project files + all ReScript dependencies. This is useful for + fixing stale caches (e.g. when upgrading ReScript versions). *:RescriptTypeHint* Uses the g:rescript_editor_support_exe executable to extract diff --git a/bsconfig.json b/examples/basic/bsconfig.json similarity index 100% rename from bsconfig.json rename to examples/basic/bsconfig.json diff --git a/examples/basic/package-lock.json b/examples/basic/package-lock.json new file mode 100644 index 0000000..a736d90 --- /dev/null +++ b/examples/basic/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "basic", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "bs-platform": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/bs-platform/-/bs-platform-8.4.2.tgz", + "integrity": "sha512-9q7S4/LLV/a68CweN382NJdCCr/lOSsJR3oQYnmPK98ChfO/AdiA3lYQkQTp6T+U0I5Z5RypUAUprNstwDtMDQ==", + "dev": true + }, + "reason-react": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/reason-react/-/reason-react-0.9.1.tgz", + "integrity": "sha512-nlH0O2TDy9KzOLOW+vlEQk4ExHOeciyzFdoLcsmmiit6hx6H5+CVDrwJ+8aiaLT/kqK5xFOjy4PS7PftWz4plA==", + "dev": true + } + } +} diff --git a/examples/basic/package.json b/examples/basic/package.json new file mode 100644 index 0000000..7d917c1 --- /dev/null +++ b/examples/basic/package.json @@ -0,0 +1,16 @@ +{ + "name": "basic", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "bs-platform": "8.4.2", + "reason-react": "0.9.1" + } +} diff --git a/examples/basic/src/Button.res b/examples/basic/src/Button.res new file mode 100644 index 0000000..ffdb3ca --- /dev/null +++ b/examples/basic/src/Button.res @@ -0,0 +1,6 @@ +@react.component +let make = () => { +
+ {React.string("Click me")} +
+} diff --git a/examples/basic/src/Json.res b/examples/basic/src/Json.res new file mode 100644 index 0000000..5732d47 --- /dev/null +++ b/examples/basic/src/Json.res @@ -0,0 +1,615 @@ +@@ocaml.doc( + " # Json parser + * + * Works with bucklescript and bsb-native + * + * ## Basics + * + * ``` + * open Json.Infix; /* for the nice infix operators */ + * let raw = {|{\"hello\": \"folks\"}|}; + * let who = Json.parse(raw) |> Json.get(\"hello\") |?> Json.string; + * Js.log(who); + * ``` + * + * ## Parse & stringify + * + * @doc parse, stringify + * + * ## Accessing descendents + * + * @doc get, nth, getPath + * + * ## Coercing to types + * + * @doc string, number, array, obj, bool, null + * + * ## The JSON type + * + * @doc t + * + * ## Infix operators for easier working + * + * @doc Infix + " +) +type rec t = + | String(string) + | Number(float) + | Array(list) + | Object(list<(string, t)>) + | True + | False + | Null + +let string_of_number = f => { + let s = Js.Float.toString(f) + if String.get(s, String.length(s) - 1) == '.' { + String.sub(s, 0, String.length(s) - 1) + } else { + s + } +} + +@ocaml.doc(" + * This module is provided for easier working with optional values. + ") +module Infix = { + @ocaml.doc( + " The \"force unwrap\" operator + * + * If you're sure there's a value, you can force it. + * ``` + * open Json.Infix; + * let x: int = Some(10) |! \"Expected this to be present\"; + * Js.log(x); + * ``` + * + * But you gotta be sure, otherwise it will throw. + * ```reason;raises + * open Json.Infix; + * let x: int = None |! \"This will throw\"; + * ``` + " + ) + let \"|!" = (o, d) => + switch o { + | None => failwith(d) + | Some(v) => v + } + @ocaml.doc( + " The \"upwrap with default\" operator + * ``` + * open Json.Infix; + * let x: int = Some(10) |? 4; + * let y: int = None |? 5; + * Js.log2(x, y); + * ``` + " + ) + let \"|?" = (o, d) => + switch o { + | None => d + | Some(v) => v + } + @ocaml.doc( + " The \"transform contents into new optional\" operator + * ``` + * open Json.Infix; + * let maybeInc = x => x > 5 ? Some(x + 1) : None; + * let x: option(int) = Some(14) |?> maybeInc; + * let y: option(int) = None |?> maybeInc; + * ``` + " + ) + let \"|?>" = (o, fn) => + switch o { + | None => None + | Some(v) => fn(v) + } + @ocaml.doc( + " The \"transform contents into new value & then re-wrap\" operator + * ``` + * open Json.Infix; + * let inc = x => x + 1; + * let x: option(int) = Some(7) |?>> inc; + * let y: option(int) = None |?>> inc; + * Js.log2(x, y); + * ``` + " + ) + let \"|?>>" = (o, fn) => + switch o { + | None => None + | Some(v) => Some(fn(v)) + } + @ocaml.doc( + " \"handle the value if present, otherwise here's the default\" + * + * It's called fold because that's what people call it :?. It's the same as \"transform contents to new value\" + \"unwrap with default\". + * + * ``` + * open Json.Infix; + * let inc = x => x + 1; + * let x: int = fold(Some(4), 10, inc); + * let y: int = fold(None, 2, inc); + * Js.log2(x, y); + * ``` + " + ) + let fold = (o, d, f) => + switch o { + | None => d + | Some(v) => f(v) + } +} + +let escape = text => { + let ln = String.length(text) + let buf = Buffer.create(ln) + let rec loop = i => + if i < ln { + switch String.get(text, i) { + | '\012' => Buffer.add_string(buf, "\\f") + | '\\' => Buffer.add_string(buf, "\\\\") + | '"' => Buffer.add_string(buf, "\\\"") + | '\n' => Buffer.add_string(buf, "\\n") + | '\b' => Buffer.add_string(buf, "\\b") + | '\r' => Buffer.add_string(buf, "\\r") + | '\t' => Buffer.add_string(buf, "\\t") + | c => Buffer.add_char(buf, c) + } + loop(i + 1) + } + loop(0) + Buffer.contents(buf) +} + +/* +let rec stringify = t => + switch t { + | String(value) => "\"" ++ (escape(value) ++ "\"") + | Number(num) => string_of_number(num) + | Array(items) => "[" ++ (String.concat(", ", () => {List.map(items, stringify)) ++ "]") + | Object(items) => + "{" ++ + (String.concat( + ", ", + List.map(items, ((k, v)) => "\"" ++ (String.escaped(k) ++ ("\": " ++ stringify(v)))), + ) ++ + "}") + | True => "true" + | False => "false" + | Null => "null" + } + */ + +@ocaml.doc( + " ``` + * let text = {|{\"hello\": \"folks\", \"aa\": [2, 3, \"four\"]}|}; + * let result = Json.stringify(Json.parse(text)); + * Js.log(result); + * assert(text == result); + * ``` + " +) +let white = n => { + let buffer = Buffer.create(n) + for _i in 0 to n - 1 { + Buffer.add_char(buffer, ' ') + } + Buffer.contents(buffer) +} + +/* let rec stringifyPretty = (~indent=0, t) => */ +/* switch t { */ +/* | String(value) => "\"" ++ (escape(value) ++ "\"") */ +/* | Number(num) => string_of_number(num) */ +/* | Array(list{}) => "[]" */ +/* | Array(items) => */ +/* "[\n" ++ */ +/* (white(indent) ++ */ +/* (String.concat(",\n" ++ white(indent), List.map(items, stringifyPretty(~indent=indent + 2))) ++ */ +/* ("\n" ++ */ +/* (white(indent) ++ "]")))) */ +/* | Object(list{}) => "{}" */ +/* | Object(items) => */ +/* "{\n" ++ */ +/* (white(indent) ++ */ +/* (String.concat( */ +/* ",\n" ++ white(indent), */ +/* List.map(items, ((k, v)) => */ +/* "\"" ++ (String.escaped(k) ++ ("\": " ++ stringifyPretty(~indent=indent + 2, v))) */ +/* ), */ +/* ) ++ */ +/* ("\n" ++ */ +/* (white(indent) ++ "}")))) */ +/* | True => "true" */ +/* | False => "false" */ +/* | Null => "null" */ +/* } */ + +let unwrap = (message, t) => + switch t { + | Some(v) => v + | None => failwith(message) + } + +@nodoc +module Parser = { + let split_by = (~keep_empty=false, is_delim, str) => { + let len = String.length(str) + let rec loop = (acc, last_pos, pos) => + if pos == -1 { + if last_pos == 0 && !keep_empty { + acc + } else { + list{String.sub(str, 0, last_pos), ...acc} + } + } else if is_delim(String.get(str, pos)) { + let new_len = last_pos - pos - 1 + if new_len != 0 || keep_empty { + let v = String.sub(str, pos + 1, new_len) + loop(list{v, ...acc}, pos, pos - 1) + } else { + loop(acc, pos, pos - 1) + } + } else { + loop(acc, last_pos, pos - 1) + } + loop(list{}, len, len - 1) + } + let fail = (text, pos, message) => { + let pre = String.sub(text, 0, pos) + let lines = split_by(c => c == '\n', pre) + let count = List.length(lines) + let last = count > 0 ? Belt.List.getExn(lines, count - 1) : "" + let col = String.length(last) + 1 + let line = List.length(lines) + let string = Printf.sprintf("Error \"%s\" at %d:%d -> %s\n", message, line, col, last) + failwith(string) + } + let rec skipToNewline = (text, pos) => + if pos >= String.length(text) { + pos + } else if String.get(text, pos) == '\n' { + pos + 1 + } else { + skipToNewline(text, pos + 1) + } + let stringTail = text => { + let len = String.length(text) + if len > 1 { + String.sub(text, 1, len - 1) + } else { + "" + } + } + let rec skipToCloseMultilineComment = (text, pos) => + if pos + 1 >= String.length(text) { + failwith("Unterminated comment") + } else if String.get(text, pos) == '*' && String.get(text, pos + 1) == '/' { + pos + 2 + } else { + skipToCloseMultilineComment(text, pos + 1) + } + let rec skipWhite = (text, pos) => + if ( + pos < String.length(text) && + (String.get(text, pos) == ' ' || + (String.get(text, pos) == '\t' || + (String.get(text, pos) == '\n' || String.get(text, pos) == '\r'))) + ) { + skipWhite(text, pos + 1) + } else { + pos + } + let parseString = (text, pos) => { + /* let i = ref(pos); */ + let buffer = Buffer.create(String.length(text)) + let ln = String.length(text) + let rec loop = i => + i >= ln + ? fail(text, i, "Unterminated string") + : switch String.get(text, i) { + | '"' => i + 1 + | '\\' => + i + 1 >= ln + ? fail(text, i, "Unterminated string") + : switch String.get(text, i + 1) { + | '/' => + Buffer.add_char(buffer, '/') + loop(i + 2) + | 'f' => + Buffer.add_char(buffer, '\012') + loop(i + 2) + | _ => + Buffer.add_string(buffer, Scanf.unescaped(String.sub(text, i, 2))) + loop(i + 2) + } + | c => + Buffer.add_char(buffer, c) + loop(i + 1) + } + let final = loop(pos) + (Buffer.contents(buffer), final) + } + let parseDigits = (text, pos) => { + let len = String.length(text) + let rec loop = i => + if i >= len { + i + } else { + switch String.get(text, i) { + | '0' .. '9' => loop(i + 1) + | _ => i + } + } + loop(pos + 1) + } + let parseWithDecimal = (text, pos) => { + let pos = parseDigits(text, pos) + if pos < String.length(text) && String.get(text, pos) == '.' { + let pos = parseDigits(text, pos + 1) + pos + } else { + pos + } + } + let parseNumber = (text, pos) => { + let pos = parseWithDecimal(text, pos) + let ln = String.length(text) + if pos < ln - 1 && (String.get(text, pos) == 'E' || String.get(text, pos) == 'e') { + let pos = switch String.get(text, pos + 1) { + | '-' | '+' => pos + 2 + | _ => pos + 1 + } + parseDigits(text, pos) + } else { + pos + } + } + let parseNegativeNumber = (text, pos) => { + let final = if String.get(text, pos) == '-' { + parseNumber(text, pos + 1) + } else { + parseNumber(text, pos) + } + (Number(float_of_string(String.sub(text, pos, final - pos))), final) + } + let expect = (char, text, pos, message) => + if String.get(text, pos) != char { + fail(text, pos, "Expected: " ++ message) + } else { + pos + 1 + } + let parseComment: 'a. (string, int, (string, int) => 'a) => 'a = (text, pos, next) => + if String.get(text, pos) != '/' { + if String.get(text, pos) == '*' { + next(text, skipToCloseMultilineComment(text, pos + 1)) + } else { + failwith("Invalid syntax") + } + } else { + next(text, skipToNewline(text, pos + 1)) + } + let maybeSkipComment = (text, pos) => + if pos < String.length(text) && String.get(text, pos) == '/' { + if pos + 1 < String.length(text) && String.get(text, pos + 1) == '/' { + skipToNewline(text, pos + 1) + } else if pos + 1 < String.length(text) && String.get(text, pos + 1) == '*' { + skipToCloseMultilineComment(text, pos + 1) + } else { + fail(text, pos, "Invalid synatx") + } + } else { + pos + } + let rec skip = (text, pos) => + if pos == String.length(text) { + pos + } else { + let n = skipWhite(text, pos) |> maybeSkipComment(text) + if n > pos { + skip(text, n) + } else { + n + } + } + let rec parse = (text, pos) => + if pos >= String.length(text) { + fail(text, pos, "Reached end of file without being done parsing") + } else { + switch String.get(text, pos) { + | '/' => parseComment(text, pos + 1, parse) + | '[' => parseArray(text, pos + 1) + | '{' => parseObject(text, pos + 1) + | 'n' => + if String.sub(text, pos, 4) == "null" { + (Null, pos + 4) + } else { + fail(text, pos, "unexpected character") + } + | 't' => + if String.sub(text, pos, 4) == "true" { + (True, pos + 4) + } else { + fail(text, pos, "unexpected character") + } + | 'f' => + if String.sub(text, pos, 5) == "false" { + (False, pos + 5) + } else { + fail(text, pos, "unexpected character") + } + | '\n' | '\t' | ' ' | '\r' => parse(text, skipWhite(text, pos)) + | '"' => + let (s, pos) = parseString(text, pos + 1) + (String(s), pos) + | '-' | '0' .. '9' => parseNegativeNumber(text, pos) + | _ => fail(text, pos, "unexpected character") + } + } + and parseArrayValue = (text, pos) => { + let pos = skip(text, pos) + let (value, pos) = parse(text, pos) + let pos = skip(text, pos) + switch String.get(text, pos) { + | ',' => + let pos = skip(text, pos + 1) + if String.get(text, pos) == ']' { + (list{value}, pos + 1) + } else { + let (rest, pos) = parseArrayValue(text, pos) + (list{value, ...rest}, pos) + } + | ']' => (list{value}, pos + 1) + | _ => fail(text, pos, "unexpected character") + } + } + and parseArray = (text, pos) => { + let pos = skip(text, pos) + switch String.get(text, pos) { + | ']' => (Array(list{}), pos + 1) + | _ => + let (items, pos) = parseArrayValue(text, pos) + (Array(items), pos) + } + } + and parseObjectValue = (text, pos) => { + let pos = skip(text, pos) + if String.get(text, pos) != '"' { + fail(text, pos, "Expected string") + } else { + let (key, pos) = parseString(text, pos + 1) + let pos = skip(text, pos) + let pos = expect(':', text, pos, "Colon") + let (value, pos) = parse(text, pos) + let pos = skip(text, pos) + switch String.get(text, pos) { + | ',' => + let pos = skip(text, pos + 1) + if String.get(text, pos) == '}' { + (list{(key, value)}, pos + 1) + } else { + let (rest, pos) = parseObjectValue(text, pos) + (list{(key, value), ...rest}, pos) + } + | '}' => (list{(key, value)}, pos + 1) + | _ => + let (rest, pos) = parseObjectValue(text, pos) + (list{(key, value), ...rest}, pos) + } + } + } + and parseObject = (text, pos) => { + let pos = skip(text, pos) + if String.get(text, pos) == '}' { + (Object(list{}), pos + 1) + } else { + let (pairs, pos) = parseObjectValue(text, pos) + (Object(pairs), pos) + } + } +} + +@ocaml.doc(" Turns some text into a json object. throws on failure ") +let parse = text => { + let (item, pos) = Parser.parse(text, 0) + let pos = Parser.skip(text, pos) + if pos < String.length(text) { + failwith( + "Extra data after parse finished: " ++ String.sub(text, pos, String.length(text) - pos), + ) + } else { + item + } +} + +/* Accessor helpers */ +let bind = (v, fn) => + switch v { + | None => None + | Some(v) => fn(v) + } + +@ocaml.doc(" If `t` is an object, get the value associated with the given string key ") +let get = (key, t) => + switch t { + | Object(items) => Belt.List.getAssoc(items, key, \"=") + | _ => None + } + +@ocaml.doc(" If `t` is an array, get the value associated with the given index ") +let nth = (n, t) => + switch t { + | Array(items) => + if n < List.length(items) { + Some(Belt.List.getExn(items, n)) + } else { + None + } + | _ => None + } + +let string = t => + switch t { + | String(s) => Some(s) + | _ => None + } + +let number = t => + switch t { + | Number(s) => Some(s) + | _ => None + } + +let array = t => + switch t { + | Array(s) => Some(s) + | _ => None + } + +let obj = t => + switch t { + | Object(s) => Some(s) + | _ => None + } + +let bool = t => + switch t { + | True => Some(true) + | False => Some(false) + | _ => None + } + +let null = t => + switch t { + | Null => Some() + | _ => None + } + +let rec parsePath = (keyList, t) => + switch keyList { + | list{} => Some(t) + | list{head, ...rest} => + switch get(head, t) { + | None => None + | Some(value) => parsePath(rest, value) + } + } + +@ocaml.doc( + " Get a deeply nested value from an object `t`. + * ``` + * open Json.Infix; + * let json = Json.parse({|{\"a\": {\"b\": {\"c\": 2}}}|}); + * let num = Json.getPath(\"a.b.c\", json) |?> Json.number; + * assert(num == Some(2.)) + * ``` + " +) +let getPath = (path, t) => { + let keys = Parser.split_by(c => c == '.', path) + parsePath(keys, t) +} diff --git a/examples/basic/src/bar.res b/examples/basic/src/bar.res new file mode 100644 index 0000000..3505ec3 --- /dev/null +++ b/examples/basic/src/bar.res @@ -0,0 +1,52 @@ +/* * this is a test */ +type t + +@bs.val external test: t => unit = "test" + +/* * Decodes a JSON value into a [bool] + +{b Returns} a [bool] if the JSON value is a [true] or [false]. +@raise [DecodeError] if unsuccessful +@example {[ + open Json + (* returns true *) + let _ = Json.parseOrRaise "true" |> Decode.bool + (* returns false *) + let _ = Json.parseOrRaise "false" |> Decode.bool + (* raises DecodeError *) + let _ = Json.parseOrRaise "123" |> Decode.bool + (* raises DecodeError *) + let _ = Json.parseOrRaise "null" |> Decode.bool +]} +*/ +let hello_world = "whatever" + +let whatever = test + +let a = (a, b) => a + b + +let hooloo = hello_world + +let abc =
+ +@react.component +let make = () => { +
{React.string("test")}
+} + +type user = {name: string} + +type what = A(string) | B(int) + +let u = {name: "test"} + +module Test = { + @react.component + let make = () => { + let inputRef = React.useRef(Js.Nullable.null) + +
+ +
+ } +} diff --git a/examples/basic/src/baz.res b/examples/basic/src/baz.res new file mode 100644 index 0000000..883df34 --- /dev/null +++ b/examples/basic/src/baz.res @@ -0,0 +1,3 @@ +let add = (a, b) => a + b + +let subst = (a, b) => a - b diff --git a/examples/basic/src/baz.resi b/examples/basic/src/baz.resi new file mode 100644 index 0000000..3ea082c --- /dev/null +++ b/examples/basic/src/baz.resi @@ -0,0 +1,3 @@ +let add: (int, int) => int + +let subst: (int, int) => int diff --git a/examples/basic/src/foo.res b/examples/basic/src/foo.res new file mode 100644 index 0000000..5aa602b --- /dev/null +++ b/examples/basic/src/foo.res @@ -0,0 +1,32 @@ +let a = 1 + +let b = "test" + +let c = Bar.hello_world + +let d = Bar.whatever + +let f = "test" + +let asdf = + +let foo = Bar.Test.make + +let fdsa = + +let plus1 = a => { + Js.log("called plus 1") + a + 1 +} + +let plus2 = a => { + Js.log("called plus 2") + a + 2 +} + +let compose = (f, g, x) => f(g(x)) + +let plus4 = plus1->compose(plus2)->compose(plus1) + +Js.log(plus4) + diff --git a/examples/basic/src/test.json b/examples/basic/src/test.json new file mode 100644 index 0000000..fdb7f36 --- /dev/null +++ b/examples/basic/src/test.json @@ -0,0 +1,28 @@ +[ + { + "hover": "int", + "range": { + "end": { + "character": 9, + "line": 0 + }, + "start": { + "character": 8, + "line": 0 + } + } + }, + { + "hover": "```\nint\n```\n\n/src/foo.res", + "range": { + "end": { + "character": 5, + "line": 0 + }, + "start": { + "character": 4, + "line": 0 + } + } + } +] diff --git a/examples/monorepo-yarn-workspaces/README.md b/examples/monorepo-yarn-workspaces/README.md new file mode 100644 index 0000000..163b3bd --- /dev/null +++ b/examples/monorepo-yarn-workspaces/README.md @@ -0,0 +1,23 @@ +# yarn workspaces monorepo example + +This example setup is using the `yarn workspace` feature in combination with ReScript's `pinned-dependencies`. + +**Expected IDE plugin behavior:** + +When the editor is opened in one of the subprojects `app` or `common`, the editor extension should correctly detect the `bsc` and `bsb` binary in the monorepo root's `node_modules` directory. + +``` +cd app +nvim . +``` + +## Setup + +``` +cd examples/monorepo-yarn-workspaces +yarn + +# Build the full project +cd app +yarn run build +``` diff --git a/examples/monorepo-yarn-workspaces/app/bsconfig.json b/examples/monorepo-yarn-workspaces/app/bsconfig.json new file mode 100644 index 0000000..a0cfea7 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/app/bsconfig.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json", + "name": "app", + "version": "1.0.0", + "sources": { + "dir" : "src", + "subdirs" : true + }, + "package-specs": { + "module": "commonjs", + "in-source": true + }, + "suffix": ".js", + "bs-dependencies": [ + "reason-react", + "common" + ], + "pinned-dependencies": ["common"], + "ppx-flags": [], + "bsc-flags": ["-bs-super-errors", "-bs-no-version-header"], + "warnings": { + "number": "-44", + "error": "+8+26+27+101" + }, + "reason": {"react-jsx": 3} +} diff --git a/examples/monorepo-yarn-workspaces/app/package.json b/examples/monorepo-yarn-workspaces/app/package.json new file mode 100644 index 0000000..bcaede8 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/app/package.json @@ -0,0 +1,16 @@ +{ + "name": "app", + "version": "0.1.0", + "private": true, + "dependencies": { + "common": "1.0.0" + }, + "scripts": { + "clean": "bsb -clean-world", + "build": "bsb -make-world", + "watch": "bsb -make-world -w" + }, + "devDependencies": { + "bs-platform": "^8.4.2" + } +} diff --git a/examples/monorepo-yarn-workspaces/app/src/App.res b/examples/monorepo-yarn-workspaces/app/src/App.res new file mode 100644 index 0000000..26f68c3 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/app/src/App.res @@ -0,0 +1,9 @@ +@react.component +let make = () => { + Common.Header.make(Js.Obj.empty())->Js.log + Common.Footer.make(Js.Obj.empty())->Js.log + + <>
{React.string("Main content")}
+} + +make(Js.Obj.empty())->Js.log diff --git a/examples/monorepo-yarn-workspaces/common/bsconfig.json b/examples/monorepo-yarn-workspaces/common/bsconfig.json new file mode 100644 index 0000000..c7d04f4 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/common/bsconfig.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json", + "name": "common", + "namespace": "Common", + "version": "0.0.1", + "sources": { + "dir" : "src", + "subdirs" : true + }, + "package-specs": { + "module": "commonjs", + "in-source": true + }, + "suffix": ".js", + "bs-dependencies": [ + "reason-react" + ], + "ppx-flags": [], + "bsc-flags": ["-bs-super-errors", "-bs-no-version-header"], + "warnings": { + "number": "-44", + "error": "+8+26+27+101" + }, + "reason": {"react-jsx": 3} +} diff --git a/examples/monorepo-yarn-workspaces/common/package.json b/examples/monorepo-yarn-workspaces/common/package.json new file mode 100644 index 0000000..34ca217 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/common/package.json @@ -0,0 +1,17 @@ +{ + "name": "common", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "clean": "bsb -clean-world", + "build": "bsb -make-world", + "watch": "bsb -make-world -w" + }, + "dependencies": { + "reason-react": "^0.9.1" + }, + "devDependencies": { + "bs-platform": "^8.4.2" + } +} diff --git a/examples/monorepo-yarn-workspaces/common/src/Footer.res b/examples/monorepo-yarn-workspaces/common/src/Footer.res new file mode 100644 index 0000000..94489e7 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/common/src/Footer.res @@ -0,0 +1,4 @@ +@react.component +let make = () => { +
{React.string("Footer")}
+} diff --git a/examples/monorepo-yarn-workspaces/common/src/Header.res b/examples/monorepo-yarn-workspaces/common/src/Header.res new file mode 100644 index 0000000..d2d0fef --- /dev/null +++ b/examples/monorepo-yarn-workspaces/common/src/Header.res @@ -0,0 +1,4 @@ +@react.component +let make = () => { +
{"Header"->React.string}
+} diff --git a/examples/monorepo-yarn-workspaces/legacy/bsconfig.json b/examples/monorepo-yarn-workspaces/legacy/bsconfig.json new file mode 100644 index 0000000..d2e5ac9 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/legacy/bsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json", + "name": "legacy", + "namespace": "Legacy", + "version": "0.0.1", + "sources": { + "dir" : "src", + "subdirs" : true + }, + "package-specs": { + "module": "commonjs", + "in-source": true + }, + "suffix": ".js", + "bs-dependencies": [], + "ppx-flags": [], + "bsc-flags": ["-bs-super-errors", "-bs-no-version-header"], + "warnings": { + "number": "-44", + "error": "+8+26+27+101" + }, + "reason": {"react-jsx": 3} +} diff --git a/examples/monorepo-yarn-workspaces/legacy/package.json b/examples/monorepo-yarn-workspaces/legacy/package.json new file mode 100644 index 0000000..e3a17c1 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/legacy/package.json @@ -0,0 +1,17 @@ +{ + "name": "legacy", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "clean": "bsb -clean-world", + "build": "bsb -make-world", + "watch": "bsb -make-world -w" + }, + "dependencies": { + "reason-react": "^0.9.1" + }, + "devDependencies": { + "bs-platform": "8.4.0" + } +} diff --git a/examples/monorepo-yarn-workspaces/legacy/src/Api.res b/examples/monorepo-yarn-workspaces/legacy/src/Api.res new file mode 100644 index 0000000..2e751b9 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/legacy/src/Api.res @@ -0,0 +1,3 @@ +let queryUser = () => { + "my-user" +} diff --git a/examples/monorepo-yarn-workspaces/package.json b/examples/monorepo-yarn-workspaces/package.json new file mode 100644 index 0000000..bdaf467 --- /dev/null +++ b/examples/monorepo-yarn-workspaces/package.json @@ -0,0 +1,11 @@ +{ + "name": "rescript-yarn-workspaces-example", + "private": true, + "workspaces": { + "packages": [ + "app", + "common", + "legacy" + ] + } +} diff --git a/examples/monorepo-yarn-workspaces/yarn.lock b/examples/monorepo-yarn-workspaces/yarn.lock new file mode 100644 index 0000000..49231de --- /dev/null +++ b/examples/monorepo-yarn-workspaces/yarn.lock @@ -0,0 +1,18 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +bs-platform@8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-8.4.0.tgz#abf4cfa260fa3b163a0f033c26adf8d5ed185b8d" + integrity sha512-00LoUWcwrtA35kQSOCD19DK55eZSggWNxVyoz1CsDhkzTh/+1hP8EQx3b+Awl0dfKnQtY5oZFYel0C/tcV7kPw== + +bs-platform@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-8.4.2.tgz#778dabd1dfb3bc95e0086c58dabae74e4ebdee8a" + integrity sha512-9q7S4/LLV/a68CweN382NJdCCr/lOSsJR3oQYnmPK98ChfO/AdiA3lYQkQTp6T+U0I5Z5RypUAUprNstwDtMDQ== + +reason-react@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/reason-react/-/reason-react-0.9.1.tgz#30a887158200b659aa03e2d75ff4cc54dc462bb0" + integrity sha512-nlH0O2TDy9KzOLOW+vlEQk4ExHOeciyzFdoLcsmmiit6hx6H5+CVDrwJ+8aiaLT/kqK5xFOjy4PS7PftWz4plA== diff --git a/ftplugin/rescript.vim b/ftplugin/rescript.vim index a9bd4fd..113f1b0 100644 --- a/ftplugin/rescript.vim +++ b/ftplugin/rescript.vim @@ -8,3 +8,11 @@ endif let b:did_ftplugin = 1 call rescript#Init() + +" On every *.res / *.resi file open, recalculate the project environment +" This helps us to always make sure that we are working off the right +" working directory etc +augroup RescriptAutoProjectEnv + au! + au BufReadPost,BufNewFile *.res, *.resi call rescript#UpdateProjectEnv() +augroup END diff --git a/package-lock.json b/package-lock.json index 9e0ec21..6acdaa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,14 @@ { - "name": "vim-rescript", - "version": "1.0.0", + "name": "vim-rescript-test", + "version": "0.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { "bs-platform": { - "version": "8.3.3-dev.2", - "resolved": "https://registry.npmjs.org/bs-platform/-/bs-platform-8.3.3-dev.2.tgz", - "integrity": "sha512-2G2PPIFhUBoN1tzZW9PurjWgvCg+Lx/gIqkAjPaDIIA6S8GFPs/UcTyl4ZwJNvQdKv33Qgih2eYTiBp4hCM3MQ==" - }, - "reason-react": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/reason-react/-/reason-react-0.9.1.tgz", - "integrity": "sha512-nlH0O2TDy9KzOLOW+vlEQk4ExHOeciyzFdoLcsmmiit6hx6H5+CVDrwJ+8aiaLT/kqK5xFOjy4PS7PftWz4plA==" + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/bs-platform/-/bs-platform-8.4.2.tgz", + "integrity": "sha512-9q7S4/LLV/a68CweN382NJdCCr/lOSsJR3oQYnmPK98ChfO/AdiA3lYQkQTp6T+U0I5Z5RypUAUprNstwDtMDQ==", + "dev": true } } } diff --git a/package.json b/package.json index 5a4c1db..9796f46 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,12 @@ { - "name": "vim-rescript", - "version": "1.0.0", - "description": "## Features", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rescript-lang/vim-rescript.git" - }, + "name": "vim-rescript-test", + "private": true, + "description": "This is only needed for our test framework", + "version": "0.0.0", "keywords": [], "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/rescript-lang/vim-rescript/issues" - }, - "homepage": "https://github.com/rescript-lang/vim-rescript#readme", - "dependencies": { - "bs-platform": "^8.3.3-dev.2", - "reason-react": "^0.9.1" + "license": "MIT", + "devDependencies": { + "bs-platform": "8.4.2" } } diff --git a/plugin/rescript.vim b/plugin/rescript.vim index 22b6afc..beb516f 100644 --- a/plugin/rescript.vim +++ b/plugin/rescript.vim @@ -5,6 +5,8 @@ let g:loaded_vim_rescript = 1 command! RescriptFormat call rescript#Format() command! RescriptBuild call rescript#BuildProject() +command! RescriptBuildWorld call rescript#BuildProject("-make-world") +command! RescriptCleanWorld call rescript#BuildProject("-clean-world") command! RescriptTypeHint call rescript#TypeHint() command! RescriptInfo call rescript#Info() command! RescriptJumpToDefinition call rescript#JumpToDefinition() diff --git a/rescript-vscode/extension.vsixmanifest b/rescript-vscode/extension.vsixmanifest index b3410ad..951e359 100644 --- a/rescript-vscode/extension.vsixmanifest +++ b/rescript-vscode/extension.vsixmanifest @@ -1,10 +1,10 @@ - + rescript-vscode The official VSCode plugin for ReScript. - rescript,language-server,ReScript,__ext_res,__ext_resi + rescript,language-server,snippet,json,ReScript,__ext_res,__ext_resi Public diff --git a/rescript-vscode/extension/HISTORY.md b/rescript-vscode/extension/HISTORY.md index e02a168..efdfb48 100644 --- a/rescript-vscode/extension/HISTORY.md +++ b/rescript-vscode/extension/HISTORY.md @@ -1,3 +1,15 @@ +## 1.0.4 + +- Some diagnostics watcher staleness fix. +- Various type hover fixes. +- Monorepo/yarn workspace support. + +## 1.0.2 + +- All the usual features (type hint, autocomplete) now work on `bsconfig.json` too! +- Snippets, to ease a few syntaxes. +- Improved highlighting for polymorphic variants. Don't abuse them please. + ## 1.0.1 - Fix temp file creation logic. diff --git a/rescript-vscode/extension/README.md b/rescript-vscode/extension/README.md index 7e055ed..769e718 100644 --- a/rescript-vscode/extension/README.md +++ b/rescript-vscode/extension/README.md @@ -2,6 +2,8 @@ The official VSCode plugin for ReScript. +![Screen shot](https://user-images.githubusercontent.com/1909539/101266821-790b1400-3707-11eb-8e9f-fb7e36e660e6.gif) + ## Prerequisite You **must** have `bs-platform 8.3.3` installed locally in your project, through the usual [npm installation](https://rescript-lang.org/docs/manual/latest/installation#integrate-into-existing-js-project). Older versions are not guaranteed to work. @@ -14,15 +16,19 @@ The plugin activates on `.res` and `.resi` files. If you've already got Reason-L ## Features -- Syntax highlighting (`.res`, `.resi`). +- Supports `.res`, `.resi` and `bsconfig.json`. +- Syntax highlighting. - Formatting, with caveats: - Currently requires the file to be part of a ReScript project, i.e. with a `bsconfig.json`. - Cannot be a temporary file. - Syntax errors diagnosis (only after formatting). - Built-in bsb watcher (optional, and exposed explicitly as a pop-up; no worries of dangling build). -- Type diagnosis. +- Type hint. - Jump to location. - Autocomplete. +- Snippets to ease a few syntaxes: + - `external` features such as `@bs.module` and `@bs.val` + - `try`, `for`, etc. ### Upcoming Features diff --git a/rescript-vscode/extension/grammars/rescript.tmLanguage.json b/rescript-vscode/extension/grammars/rescript.tmLanguage.json index 747f9ec..2de2fca 100644 --- a/rescript-vscode/extension/grammars/rescript.tmLanguage.json +++ b/rescript-vscode/extension/grammars/rescript.tmLanguage.json @@ -194,7 +194,7 @@ "operator": { "patterns": [ { - "match": "->|\\|\\||&&|\\+\\+|\\*\\*|\\+\\.|\\+|-\\.|-|\\*\\.|\\*|/\\.|/|\\.\\.\\.|\\.\\.|\\|>|===|==|\\^|:=|!|>=|<=", + "match": "->|\\|\\||&&|\\+\\+|\\*\\*|\\+\\.|\\+|-\\.|-|\\*\\.|\\*|/\\.|/|\\.\\.\\.|\\.\\.|\\|>|===|==|\\^|:=|!|>=(?! *\\?)|<=", "name": "keyword.operator" } ] @@ -214,13 +214,16 @@ "name": "variable.function variable.other" }, { - "match": "(#)(\\.\\.\\.)?[a-zA-Z][0-9a-zA-Z_]*\\b", + "match": "(#)(\\.\\.\\.)?([a-zA-Z][0-9a-zA-Z_]*)\\b", "captures": { "1": { "name": "punctuation.definition.keyword" }, "2": { "name": "punctuation.definition.keyword" + }, + "3": { + "name": "variable.function variable.other" } } } diff --git a/rescript-vscode/extension/package.json b/rescript-vscode/extension/package.json index 73948c3..4c29a38 100644 --- a/rescript-vscode/extension/package.json +++ b/rescript-vscode/extension/package.json @@ -3,7 +3,7 @@ "description": "The official VSCode plugin for ReScript.", "author": "chenglou", "license": "MIT", - "version": "1.0.1", + "version": "1.0.4", "repository": { "type": "git", "url": "https://github.com/rescript-lang/rescript-vscode" @@ -23,6 +23,18 @@ ], "main": "./client/out/extension", "contributes": { + "jsonValidation": [ + { + "fileMatch": "bsconfig.json", + "url": "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json" + } + ], + "snippets": [ + { + "language": "rescript", + "path": "./snippets.json" + } + ], "taskDefinitions_unused": [ { "type": "bsb", diff --git a/rescript-vscode/extension/server/darwin/rescript-editor-support.exe b/rescript-vscode/extension/server/darwin/rescript-editor-support.exe index b54b12a..a014d87 100755 Binary files a/rescript-vscode/extension/server/darwin/rescript-editor-support.exe and b/rescript-vscode/extension/server/darwin/rescript-editor-support.exe differ diff --git a/rescript-vscode/extension/server/linux/rescript-editor-support.exe b/rescript-vscode/extension/server/linux/rescript-editor-support.exe index c724611..b373fa4 100755 Binary files a/rescript-vscode/extension/server/linux/rescript-editor-support.exe and b/rescript-vscode/extension/server/linux/rescript-editor-support.exe differ diff --git a/rescript-vscode/extension/server/out/constants.js b/rescript-vscode/extension/server/out/constants.js index 31d3deb..a251ab3 100644 --- a/rescript-vscode/extension/server/out/constants.js +++ b/rescript-vscode/extension/server/out/constants.js @@ -19,15 +19,15 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.startBuildAction = exports.resiExt = exports.resExt = exports.compilerLogPartialPath = exports.bsconfigPartialPath = exports.bsbLock = exports.bsbPartialPath = exports.bscPartialPath = exports.jsonrpcVersion = void 0; +exports.startBuildAction = exports.resiExt = exports.resExt = exports.compilerLogPartialPath = exports.bsconfigPartialPath = exports.bsbLock = exports.bsbNodePartialPath = exports.bscExePartialPath = exports.jsonrpcVersion = void 0; const path = __importStar(require("path")); // See https://microsoft.github.io/language-server-protocol/specification Abstract Message // version is fixed to 2.0 exports.jsonrpcVersion = "2.0"; -exports.bscPartialPath = path.join("node_modules", "bs-platform", process.platform, "bsc.exe"); +exports.bscExePartialPath = path.join("node_modules", "bs-platform", process.platform, "bsc.exe"); // can't use the native bsb since we might need the watcher -w flag, which is only in the js wrapper // export let bsbPartialPath = path.join('node_modules', 'bs-platform', process.platform, 'bsb.exe'); -exports.bsbPartialPath = path.join("node_modules", ".bin", "bsb"); +exports.bsbNodePartialPath = path.join("node_modules", ".bin", "bsb"); exports.bsbLock = ".bsb.lock"; exports.bsconfigPartialPath = "bsconfig.json"; exports.compilerLogPartialPath = path.join("lib", "bs", ".compiler.log"); diff --git a/rescript-vscode/extension/server/out/server.js b/rescript-vscode/extension/server/out/server.js index d29f39e..2750def 100644 --- a/rescript-vscode/extension/server/out/server.js +++ b/rescript-vscode/extension/server/out/server.js @@ -102,7 +102,13 @@ let deleteProjectDiagnostics = (projectRootPath) => { projectsFiles.delete(projectRootPath); } }; -let compilerLogsWatcher = chokidar.watch([]).on("all", (_e, changedPath) => { +let compilerLogsWatcher = chokidar + .watch([], { + awaitWriteFinish: { + stabilityThreshold: 1, + }, +}) + .on("all", (_e, changedPath) => { sendUpdatedDiagnostics(); }); let stopWatchingCompilerLog = () => { @@ -129,7 +135,7 @@ let openedFile = (fileUri, fileContent) => { // because otherwise the diagnostics info we'll display might be stale let bsbLockPath = path.join(projectRootPath, c.bsbLock); if (firstOpenFileOfProject && !fs_1.default.existsSync(bsbLockPath)) { - let bsbPath = path.join(projectRootPath, c.bsbPartialPath); + let bsbPath = path.join(projectRootPath, c.bsbNodePartialPath); // TODO: sometime stale .bsb.lock dangling. bsb -w knows .bsb.lock is // stale. Use that logic // TODO: close watcher when lang-server shuts down @@ -406,11 +412,13 @@ process_1.default.on("message", (msg) => { process_1.default.send(response); } else { - let projectRootPath = utils.findProjectRootOfFile(filePath); - if (projectRootPath == null) { + // See comment on findBscExeDirOfFile for why we need + // to recursively search for bsc.exe upward + let bscExeDir = utils.findBscExeDirOfFile(filePath); + if (bscExeDir === null) { let params = { type: p.MessageType.Error, - message: `Cannot find a nearby ${c.bsconfigPartialPath}. It's needed for determining the project's root.`, + message: `Cannot find a nearby ${c.bscExePartialPath}. It's needed for formatting.`, }; let response = { jsonrpc: c.jsonrpcVersion, @@ -421,51 +429,36 @@ process_1.default.on("message", (msg) => { process_1.default.send(response); } else { - let bscPath = path.join(projectRootPath, c.bscPartialPath); - if (!fs_1.default.existsSync(bscPath)) { - let params = { - type: p.MessageType.Error, - message: `Cannot find a nearby ${c.bscPartialPath}. It's needed for formatting.`, - }; + let resolvedBscPath = path.join(bscExeDir, c.bscExePartialPath); + // code will always be defined here, even though technically it can be undefined + let code = getOpenedFileContent(params.textDocument.uri); + let formattedResult = utils.formatUsingValidBscPath(code, resolvedBscPath, extension === c.resiExt); + if (formattedResult.kind === "success") { + let result = [ + { + range: { + start: { line: 0, character: 0 }, + end: { + line: Number.MAX_VALUE, + character: Number.MAX_VALUE, + }, + }, + newText: formattedResult.result, + }, + ]; let response = { jsonrpc: c.jsonrpcVersion, - method: "window/showMessage", - params: params, + id: msg.id, + result: result, }; - process_1.default.send(fakeSuccessResponse); process_1.default.send(response); } else { - // code will always be defined here, even though technically it can be undefined - let code = getOpenedFileContent(params.textDocument.uri); - let formattedResult = utils.formatUsingValidBscPath(code, bscPath, extension === c.resiExt); - if (formattedResult.kind === "success") { - let result = [ - { - range: { - start: { line: 0, character: 0 }, - end: { - line: Number.MAX_VALUE, - character: Number.MAX_VALUE, - }, - }, - newText: formattedResult.result, - }, - ]; - let response = { - jsonrpc: c.jsonrpcVersion, - id: msg.id, - result: result, - }; - process_1.default.send(response); - } - else { - // let the diagnostics logic display the updated syntax errors, - // from the build. - // Again, not sending the actual errors. See fakeSuccessResponse - // above for explanation - process_1.default.send(fakeSuccessResponse); - } + // let the diagnostics logic display the updated syntax errors, + // from the build. + // Again, not sending the actual errors. See fakeSuccessResponse + // above for explanation + process_1.default.send(fakeSuccessResponse); } } } @@ -493,7 +486,7 @@ process_1.default.on("message", (msg) => { msg.result.title === c.startBuildAction) { let msg_ = msg.result; let projectRootPath = msg_.projectRootPath; - let bsbPath = path.join(projectRootPath, c.bsbPartialPath); + let bsbPath = path.join(projectRootPath, c.bsbNodePartialPath); // TODO: sometime stale .bsb.lock dangling // TODO: close watcher when lang-server shuts down if (fs_1.default.existsSync(bsbPath)) { diff --git a/rescript-vscode/extension/server/out/utils.js b/rescript-vscode/extension/server/out/utils.js index 5ccc3be..2f0bb99 100644 --- a/rescript-vscode/extension/server/out/utils.js +++ b/rescript-vscode/extension/server/out/utils.js @@ -22,7 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseCompilerLogOutput = exports.parseDiagnosticLocation = exports.runBsbWatcherUsingValidBsbPath = exports.formatUsingValidBscPath = exports.findProjectRootOfFile = exports.createFileInTempDir = void 0; +exports.parseCompilerLogOutput = exports.parseDiagnosticLocation = exports.runBsbWatcherUsingValidBsbPath = exports.formatUsingValidBscPath = exports.findBscExeDirOfFile = exports.findProjectRootOfFile = exports.createFileInTempDir = void 0; const c = __importStar(require("./constants")); const childProcess = __importStar(require("child_process")); const path = __importStar(require("path")); @@ -53,6 +53,31 @@ exports.findProjectRootOfFile = (source) => { } } }; +// TODO: races here? +// TODO: this doesn't handle file:/// scheme +// We need to recursively search for bs-platform/{platform}/bsc.exe upward from +// the project's root, because in some setups, such as yarn workspace/monorepo, +// the node_modules/bs-platform package might be hoisted up instead of alongside +// the project root. +// Also, if someone's ever formatting a regular project setup's dependency +// (which is weird but whatever), they'll at least find an upward bs-platform +// from the dependent. +exports.findBscExeDirOfFile = (source) => { + let dir = path.dirname(source); + let bscPath = path.join(dir, c.bscExePartialPath); + if (fs_1.default.existsSync(bscPath)) { + return dir; + } + else { + if (dir === source) { + // reached the top + return null; + } + else { + return exports.findBscExeDirOfFile(dir); + } + } +}; exports.formatUsingValidBscPath = (code, bscPath, isInterface) => { let extension = isInterface ? c.resiExt : c.resExt; let formatTempFileFullPath = exports.createFileInTempDir(extension); diff --git a/rescript-vscode/extension/server/win32/rescript-editor-support.exe b/rescript-vscode/extension/server/win32/rescript-editor-support.exe index 583d560..26ce3ad 100755 Binary files a/rescript-vscode/extension/server/win32/rescript-editor-support.exe and b/rescript-vscode/extension/server/win32/rescript-editor-support.exe differ diff --git a/rescript-vscode/extension/snippets.json b/rescript-vscode/extension/snippets.json new file mode 100644 index 0000000..303feee --- /dev/null +++ b/rescript-vscode/extension/snippets.json @@ -0,0 +1,91 @@ +{ + "Module": { + "prefix": [ + "module" + ], + "body": [ + "module ${1:Name} = {", + "\t${2:// Module contents}", + "}" + ] + }, + "Switch": { + "prefix": [ + "switch" + ], + "body": [ + "switch ${1:value} {", + "| ${2:pattern1} => ${3:expression}", + "${4:| ${5:pattern2} => ${6:expression}}", + "}" + ] + }, + "Try": { + "prefix": [ + "try" + ], + "body": [ + "try {", + "\t${1:expression}", + "} catch {", + "| ${2:MyException} => ${3:expression}", + "}" + ] + }, + "For Loop": { + "prefix": [ + "for" + ], + "body": [ + "for ${1:i} in ${2:startValueInclusive} to ${3:endValueInclusive} {", + "\t${4:Js.log(${1:i})}", + "}" + ] + }, + "Reverse For Loop": { + "prefix": [ + "for" + ], + "body": [ + "for ${1:i} in ${2:startValueInclusive} downto ${3:endValueInclusive} {", + "\t${4:Js.log(${1:i})}", + "}" + ] + }, + "Global External Object": { + "prefix": [ + "@bs", + "external" + ], + "body": [ + "@val external ${1:setTimeout}: ${2:(unit => unit, int) => float} = \"${3:setTimeout}\"" + ] + }, + "Global External Module": { + "prefix": [ + "@bs", + "external" + ], + "body": [ + "@scope(\"${1:Math}\") @val external ${2:random}: ${3:unit => float} = \"${4:random}\"" + ] + }, + "JS Module External": { + "prefix": [ + "@bs", + "external" + ], + "body": [ + "@module(\"${1:path}\") external ${2:dirname}: ${3:string => string} = \"${4:dirname}\"" + ] + }, + "JS Module Default External": { + "prefix": [ + "@bs", + "external" + ], + "body": [ + "@module external ${1:leftPad}: ${2:(string, int) => string} = \"${3:leftPad}\"" + ] + } +}