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
+
+ <>