diff --git a/src/Client.re b/src/Client.re index 76d15c9..640d07f 100644 --- a/src/Client.re +++ b/src/Client.re @@ -35,8 +35,7 @@ let start = Log.info("Ready"); incr(lastRequestId); - let requestId = lastRequestId^; - send(Outgoing.Initialize({requestId, initData})); + send(Outgoing.Initialize({requestId: lastRequestId^, initData})); handler(Ready) |> ignore; | Incoming.Initialized => @@ -46,10 +45,9 @@ let start = "ExtHostConfiguration" |> Handlers.stringToId |> Option.get; incr(lastRequestId); - let requestId = lastRequestId^; send( Outgoing.RequestJSONArgs({ - requestId, + requestId: lastRequestId^, rpcId, method: "$initializeConfiguration", args: @@ -62,11 +60,10 @@ let start = handler(Initialized) |> ignore; incr(lastRequestId); - let requestId = lastRequestId^; let rpcId = "ExtHostWorkspace" |> Handlers.stringToId |> Option.get; send( Outgoing.RequestJSONArgs({ - requestId, + requestId: lastRequestId^, rpcId, method: "$initializeWorkspace", args: `List([]), diff --git a/src/Core/KeyedStringMap.re b/src/Core/KeyedStringMap.re new file mode 100644 index 0000000..8addfe8 --- /dev/null +++ b/src/Core/KeyedStringMap.re @@ -0,0 +1,24 @@ +module Key: { + type t = + pri { + hash: int, + name: string, + }; + let compare: (t, t) => int; + let create: string => t; +} = { + type t = { + hash: int, + name: string, + }; + + let compare = (a, b) => + a.hash == b.hash ? compare(a.name, b.name) : compare(a.hash, b.hash); + + let create = name => {hash: Hashtbl.hash(name), name}; +}; + +include Map.Make(Key); + +let key = Key.create; +let keyName = (key: key) => key.name; diff --git a/src/Core/KeyedStringMap.rei b/src/Core/KeyedStringMap.rei new file mode 100644 index 0000000..eca49ad --- /dev/null +++ b/src/Core/KeyedStringMap.rei @@ -0,0 +1,39 @@ +type key; +let key: string => key; +let keyName: key => string; + +type t(+'a); + +let empty: t('a); +let is_empty: t('a) => bool; + +let mem: (key, t('a)) => bool; +let add: (key, 'a, t('a)) => t('a); +let update: (key, option('a) => option('a), t('a)) => t('a); +let remove: (key, t('a)) => t('a); +let merge: + ((key, option('a), option('b)) => option('c), t('a), t('b)) => t('c); +let union: ((key, 'a, 'a) => option('a), t('a), t('a)) => t('a); + +let compare: (('a, 'a) => int, t('a), t('a)) => int; +let equal: (('a, 'a) => bool, t('a), t('a)) => bool; + +let iter: ((key, 'a) => unit, t('a)) => unit; +let fold: ((key, 'a, 'b) => 'b, t('a), 'b) => 'b; + +let for_all: ((key, 'a) => bool, t('a)) => bool; +let exists: ((key, 'a) => bool, t('a)) => bool; + +let filter: ((key, 'a) => bool, t('a)) => t('a); +let partition: ((key, 'a) => bool, t('a)) => (t('a), t('a)); + +let find_opt: (key, t('a)) => option('a); +let find_first_opt: (key => bool, t('a)) => option((key, 'a)); +let find_last_opt: (key => bool, t('a)) => option((key, 'a)); + +let map: ('a => 'b, t('a)) => t('b); +let mapi: ((key, 'a) => 'b, t('a)) => t('b); + +let to_seq: t('a) => Seq.t((key, 'a)); +let add_seq: (Seq.t((key, 'a)), t('a)) => t('a); +let of_seq: Seq.t((key, 'a)) => t('a); diff --git a/src/Core/KeyedStringTree.re b/src/Core/KeyedStringTree.re new file mode 100644 index 0000000..c2989f9 --- /dev/null +++ b/src/Core/KeyedStringTree.re @@ -0,0 +1,4 @@ +include KeyedTree.Make(String); + +let path = String.split_on_char('.'); +let key = String.concat("."); diff --git a/src/Core/KeyedStringTree.rei b/src/Core/KeyedStringTree.rei new file mode 100644 index 0000000..d174c55 --- /dev/null +++ b/src/Core/KeyedStringTree.rei @@ -0,0 +1,4 @@ +include KeyedTree.S with type key = string; + +let path: string => path; +let key: path => string; diff --git a/src/Core/KeyedTree.re b/src/Core/KeyedTree.re new file mode 100644 index 0000000..e683feb --- /dev/null +++ b/src/Core/KeyedTree.re @@ -0,0 +1,179 @@ +module type OrderedType = Map.OrderedType; + +module type S = { + type key; + type path = list(key); + module KeyedMap: Map.S with type key := key; + type t('a) = + | Node(KeyedMap.t(t('a))) + | Leaf('a); + + let empty: t(_); + + let fromList: list((path, 'a)) => t('a); + + let add: (path, 'a, t('a)) => t('a); + let get: (path, t('a)) => option('a); + + let merge: + ((path, option('a), option('b)) => option('c), t('a), t('b)) => t('c); + let union: ((path, 'a, 'a) => option('a), t('a), t('a)) => t('a); + let map: ('a => 'b, t('a)) => t('b); + let fold: ((path, 'a, 'b) => 'b, t('a), 'b) => 'b; +}; + +module Make = (Ord: OrderedType) => { + type key = Ord.t; + type path = list(key); + module KeyedMap = Map.Make(Ord); + type t('a) = + | Node(KeyedMap.t(t('a))) + | Leaf('a); + + let empty = Node(KeyedMap.empty); + + let rec update = (path, value: option('a), node: t('a)) => { + switch (path) { + | [] => node + + | [key] => + switch (node) { + | Node(children) => + Node( + KeyedMap.update( + key, + fun + | None => Option.map(value => Leaf(value), value) + | Some(Leaf(_)) => Option.map(value => Leaf(value), value) // duplicate, override existing value + | Some(Node(_)) as node => node, // ignore if internal node, NOTE: lossy + children, + ), + ) + | Leaf(_) => + switch (value) { + | Some(value) => Node(KeyedMap.singleton(key, Leaf(value))) + | None => Node(KeyedMap.empty) + } + } + + | [key, ...rest] => + switch (node) { + | Node(children) => + let newChildren = + KeyedMap.update( + key, + fun + | Some(Node(_) as child) => Some(update(rest, value, child)) + | None + | Some(Leaf(_)) => Some(update(rest, value, empty)), // override leaf, NOTE: lossy + children, + ); + + Node(newChildren); + + | Leaf(_) => + // override leaf, NOTE: lossy + let child = update(rest, value, empty); + Node(KeyedMap.singleton(key, child)); + } + }; + }; + + let add = (keys, value) => update(keys, Some(value)); + + let fromList = entries => + List.fold_left( + (acc, (key, value)) => add(key, value, acc), + empty, + entries, + ); + + let rec get = (path, node) => + switch (path, node) { + | ([], Leaf(value)) => Some(value) + | ([key, ...rest], Node(children)) => + switch (KeyedMap.find_opt(key, children)) { + | Some(child) => get(rest, child) + | None => None + } + | _ => None + }; + + let merge = (f, a, b) => { + let f = (path, a, b) => + Option.map(value => Leaf(value), f(path, a, b)); + + let rec aux = (path, a, b) => + switch (a, b) { + | (Leaf(a), Leaf(b)) => f(path, Some(a), Some(b)) + | (Node(_), Leaf(b)) => f(path, None, Some(b)) + | (Leaf(a), Node(_)) => f(path, Some(a), None) + | (Node(aChildren), Node(bChildren)) => + let merged = + KeyedMap.merge( + (key, a, b) => { + let path = [key, ...path]; + switch (a, b) { + | (Some(a), Some(b)) => aux(path, a, b) + | (None, Some(b)) => aux(path, empty, b) + | (Some(a), None) => aux(path, a, empty) + | (None, None) => failwith("unreachable") + }; + }, + aChildren, + bChildren, + ); + + if (merged == KeyedMap.empty) { + None; + } else { + Some(Node(merged)); + }; + }; + + aux([], a, b) |> Option.value(~default=empty); + }; + + let union = f => { + let rec aux = (f, path, a, b) => + switch (a, b) { + | (Leaf(_), Leaf(b)) => Leaf(b) // duplicate, override a with b + | (Node(_), Leaf(_)) => a // Node always wins, NOTE: lossy + | (Leaf(_), Node(_)) => b // Node always wins, NOTE: lossy + | (Node(aChildren), Node(bChildren)) => + // merge children + let merged = + KeyedMap.union( + (key, a, b) => { + let path = [key, ...path]; + Some(aux(f, path, a, b)); + }, + aChildren, + bChildren, + ); + Node(merged); + }; + + aux(f, []); + }; + + let rec map = f => + fun + | Leaf(value) => Leaf(f(value)) + | Node(children) => Node(KeyedMap.map(map(f), children)); + + let fold = (f, node, initial) => { + let rec traverse = (node, revPath, acc) => + switch (node) { + | Leaf(value) => f(List.rev(revPath), value, acc) + | Node(children) => + KeyedMap.fold( + (key, node, acc) => traverse(node, [key, ...revPath], acc), + children, + acc, + ) + }; + + traverse(node, [], initial); + }; +}; diff --git a/src/Core/dune b/src/Core/dune new file mode 100644 index 0000000..4b0c148 --- /dev/null +++ b/src/Core/dune @@ -0,0 +1,6 @@ +(library + (name Core) + (public_name vscode-exthost.core) + (library_flags (-linkall)) + (libraries luv yojson decoders-yojson) + (preprocess (pps ppx_deriving.show ppx_deriving_yojson))) diff --git a/src/Core/keyedTree.rei b/src/Core/keyedTree.rei new file mode 100644 index 0000000..842f938 --- /dev/null +++ b/src/Core/keyedTree.rei @@ -0,0 +1,25 @@ +module type OrderedType = Map.OrderedType; + +module type S = { + type key; + type path = list(key); + module KeyedMap: Map.S with type key := key; + type t('a) = + | Node(KeyedMap.t(t('a))) + | Leaf('a); + + let empty: t(_); + + let fromList: list((path, 'a)) => t('a); + + let add: (path, 'a, t('a)) => t('a); + let get: (path, t('a)) => option('a); + + let merge: + ((path, option('a), option('b)) => option('c), t('a), t('b)) => t('c); + let union: ((path, 'a, 'a) => option('a), t('a), t('a)) => t('a); + let map: ('a => 'b, t('a)) => t('b); + let fold: ((path, 'a, 'b) => 'b, t('a), 'b) => 'b; +}; + +module Make: (Ord: OrderedType) => S with type key = Ord.t; diff --git a/src/Extension/Contributions.re b/src/Extension/Contributions.re index 64a3edf..d0b7691 100644 --- a/src/Extension/Contributions.re +++ b/src/Extension/Contributions.re @@ -1,9 +1,8 @@ /* - * Contributions.re + * ExtensionContributions.re * * Types for VSCode Extension contribution points */ - open Types; open Rench; @@ -13,6 +12,7 @@ module Command = { command: string, title: LocalizedToken.t, category: option(string), + condition: WhenExpr.t, }; let decode = @@ -22,6 +22,12 @@ module Command = { command: field.required("command", string), title: field.required("title", LocalizedToken.decode), category: field.optional("category", string), + condition: + field.withDefault( + "when", + WhenExpr.Value(True), + string |> map(WhenExpr.parse), + ), } ) ); @@ -36,6 +42,39 @@ module Command = { ); }; +module Menu = { + [@deriving show] + type t = Menu.Schema.definition + + and item = Menu.Schema.item; + + module Decode = { + open Json.Decode; + + let whenExpression = string |> map(WhenExpr.parse); + + let item = + obj(({field, _}) => + Menu.Schema.{ + command: field.required("command", string), + alt: field.optional("alt", string), + group: field.optional("group", string), + index: None, + isVisibleWhen: + field.withDefault( + "when", + WhenExpr.Value(True), + string |> map(WhenExpr.parse), + ), + } + ); + + let menus = + key_value_pairs(list(item)) + |> map(List.map(((id, items)) => Menu.Schema.{id, items})); + }; +}; + module Configuration = { [@deriving show] type t = list(property) @@ -70,6 +109,11 @@ module Configuration = { }; let decode = Decode.configuration; + + let toSettings = config => + config + |> List.map(({name, default}) => (name, default)) + |> Config.Settings.fromList; }; module Language = { @@ -205,35 +249,38 @@ module IconTheme = { [@deriving show] type t = { + configuration: Configuration.t, commands: list(Command.t), + menus: list(Menu.t), languages: list(Language.t), grammars: list(Grammar.t), themes: list(Theme.t), iconThemes: list(IconTheme.t), - configuration: Configuration.t, }; let default = { + configuration: [], commands: [], + menus: [], languages: [], grammars: [], themes: [], iconThemes: [], - configuration: [], }; let decode = Json.Decode.( obj(({field, _}) => { + configuration: + field.withDefault("configuration", [], Configuration.decode), commands: field.withDefault("commands", [], list(Command.decode)), + menus: field.withDefault("menus", [], Menu.Decode.menus), languages: field.withDefault("languages", [], list(Language.decode)), grammars: field.withDefault("grammars", [], list(Grammar.decode)), themes: field.withDefault("themes", [], list(Theme.decode)), iconThemes: field.withDefault("iconThemes", [], list(IconTheme.decode)), - configuration: - field.withDefault("configuration", [], Configuration.decode), } ) ); @@ -241,12 +288,13 @@ let decode = let encode = data => Json.Encode.( obj([ + ("configuration", null), ("commands", data.commands |> list(Command.encode)), + ("menus", null), ("languages", data.languages |> list(Language.encode)), ("grammars", data.grammars |> list(Grammar.encode)), ("themes", data.themes |> list(Theme.encode)), ("iconThemes", data.iconThemes |> list(IconTheme.encode)), - ("configuration", null), ]) ); diff --git a/src/Extension/Extension.rei b/src/Extension/Extension.rei index 43b2eef..c570cd8 100644 --- a/src/Extension/Extension.rei +++ b/src/Extension/Extension.rei @@ -41,6 +41,7 @@ module Contributions: { command: string, title: LocalizedToken.t, category: option(string), + condition: WhenExpr.t, }; }; @@ -61,6 +62,8 @@ module Contributions: { }; }; + module Menu: {type t = Types.Menu.Schema.definition;}; + module Grammar: { [@deriving show] type t = { @@ -91,13 +94,16 @@ module Contributions: { [@deriving show] type t = { + configuration: Configuration.t, commands: list(Command.t), + menus: list(Menu.t), languages: list(Language.t), grammars: list(Grammar.t), themes: list(Theme.t), iconThemes: list(IconTheme.t), - configuration: Configuration.t, }; + + let encode: Types.Json.encoder(t); }; module Manifest: { @@ -128,6 +134,10 @@ module Manifest: { | Workspace; let decode: Types.Json.decoder(t); + + module Encode: {let kind: Types.Json.encoder(kind);}; + + let getDisplayName: t => string; }; module Scanner: { diff --git a/src/Extension/Manifest.re b/src/Extension/Manifest.re index 3b50dbb..e7a01f9 100644 --- a/src/Extension/Manifest.re +++ b/src/Extension/Manifest.re @@ -109,7 +109,6 @@ let getDisplayName = (manifest: t) => { let remapPaths = (rootPath: string, manifest: t) => { ...manifest, - //main: Option.map(Path.join(rootPath), manifest.main), icon: Option.map(Path.join(rootPath), manifest.icon), contributes: Contributions.remapPaths(rootPath, manifest.contributes), }; diff --git a/src/Extension/dune b/src/Extension/dune index 5470285..7508db9 100644 --- a/src/Extension/dune +++ b/src/Extension/dune @@ -1,5 +1,5 @@ (library (name Extension) (public_name vscode-exthost.extension) - (libraries Rench luv re timber yojson decoders-yojson vscode-exthost.types) + (libraries Rench luv re timber yojson decoders-yojson vscode-exthost.whenexpr vscode-exthost.types) (preprocess (pps ppx_deriving.show ppx_deriving_yojson))) diff --git a/src/Exthost.re b/src/Exthost.re index b697cc3..cd8af3c 100644 --- a/src/Exthost.re +++ b/src/Exthost.re @@ -11,6 +11,8 @@ module NamedPipe = NamedPipe; module Msg = Msg; +module Extension = Extension; module Protocol = Protocol; module Transport = Transport; module Utility = Utility; +module WhenExpr = WhenExpr; diff --git a/src/Exthost.rei b/src/Exthost.rei index e21e6a2..4ce74bc 100644 --- a/src/Exthost.rei +++ b/src/Exthost.rei @@ -113,6 +113,8 @@ module Request: { }; }; +module Extension = Extension; module Protocol = Protocol; module Transport = Transport; module Utility = Utility; +module WhenExpr = WhenExpr; diff --git a/src/Types/Command.re b/src/Types/Command.re new file mode 100644 index 0000000..2c69260 --- /dev/null +++ b/src/Types/Command.re @@ -0,0 +1,59 @@ +module Log = (val Timber.Log.withNamespace("Oni2.Core.Command")); + +[@deriving show] +type t('msg) = { + id: string, + title: option(string), + category: option(string), + icon: option([@opaque] IconTheme.IconDefinition.t), + isEnabledWhen: WhenExpr.t, + msg: [ | `Arg0('msg) | `Arg1(Json.t => 'msg)], +}; + +let map = (f, command) => { + ...command, + msg: + switch (command.msg) { + | `Arg0(msg) => `Arg0(f(msg)) + | `Arg1(msgf) => `Arg1(arg => f(msgf(arg))) + }, +}; + +module Lookup = { + type command('msg) = t('msg); + type t('msg) = KeyedStringMap.t(command('msg)); + + let fromList = commands => + commands + |> List.to_seq + |> Seq.map(command => (KeyedStringMap.key(command.id), command)) + |> KeyedStringMap.of_seq; + + let get = (key, lookup) => + KeyedStringMap.find_opt(KeyedStringMap.key(key), lookup); + + let add = (key, command, lookup) => + KeyedStringMap.add(KeyedStringMap.key(key), command, lookup); + + let union = (xs, ys) => + KeyedStringMap.union( + (key, _x, y) => { + Log.warnf(m => + m("Encountered duplicate command: %s", KeyedStringMap.keyName(key)) + ); + Some(y); + }, + xs, + ys, + ); + + let unionMany = lookups => + List.fold_left(union, KeyedStringMap.empty, lookups); + + let map = (f, lookup) => KeyedStringMap.map(map(f), lookup); + + let toList = lookup => + KeyedStringMap.to_seq(lookup) + |> Seq.map(((_key, definition)) => definition) + |> List.of_seq; +}; diff --git a/src/Types/Command.rei b/src/Types/Command.rei new file mode 100644 index 0000000..ff28ac5 --- /dev/null +++ b/src/Types/Command.rei @@ -0,0 +1,28 @@ +[@deriving show] +type t('msg) = { + id: string, + title: option(string), + category: option(string), + icon: option([@opaque] IconTheme.IconDefinition.t), + isEnabledWhen: WhenExpr.t, + msg: [ | `Arg0('msg) | `Arg1(Json.t => 'msg)], +}; + +let map: ('a => 'b, t('a)) => t('b); + +module Lookup: { + type command('msg) = t('msg); + type t('msg); + + let fromList: list(command('msg)) => t('msg); + + let get: (string, t('msg)) => option(command('msg)); + let add: (string, command('msg), t('msg)) => t('msg); + + let union: (t('msg), t('msg)) => t('msg); + let unionMany: list(t('msg)) => t('msg); + + let map: ('a => 'b, t('a)) => t('b); + + let toList: t('msg) => list(command('msg)); +}; diff --git a/src/Types/Config.re b/src/Types/Config.re new file mode 100644 index 0000000..6b0af8d --- /dev/null +++ b/src/Types/Config.re @@ -0,0 +1,165 @@ +module Log = (val Timber.Log.withNamespace("Oni2.Core.Config")); +module Lookup = KeyedStringTree; + +type key = Lookup.path; +type resolver = key => option(Json.t); + +let key = Lookup.path; +let keyAsString = Lookup.key; + +// SETTINGS + +module Settings = { + type t = Lookup.t(Json.t); + + let empty = Lookup.empty; + + let fromList = entries => + entries + |> List.map(((key, entry)) => (Lookup.path(key), entry)) + |> Lookup.fromList; + + let fromJson = json => { + switch (json) { + | `Assoc(items) => fromList(items) + + | _ => + Log.errorf(m => m("Expected file to contain a JSON object")); + empty; + }; + }; + + let fromFile = path => + try(path |> Yojson.Safe.from_file |> fromJson) { + | Yojson.Json_error(message) => + Log.errorf(m => m("Failed to read file %s: %s", path, message)); + empty; + }; + + let get = Lookup.get; + + let union = (xs, ys) => + Lookup.union( + (path, _x, y) => { + Log.warnf(m => m("Encountered duplicate key: %s", Lookup.key(path))); + Some(y); + }, + xs, + ys, + ); + let unionMany = lookups => List.fold_left(union, Lookup.empty, lookups); + + let diff = (xs, ys) => + Lookup.merge( + (_path, x, y) => + switch (x, y) { + | (Some(x), Some(y)) when x == y => None + | (Some(_), Some(y)) => Some(y) + | (Some(_), None) => Some(Json.Encode.null) + | (None, Some(value)) => Some(value) + | (None, None) => failwith("unreachable") + }, + xs, + ys, + ); + + let changed = (xs, ys) => + diff(xs, ys) |> Lookup.map(_ => Json.Encode.bool(true)); + + let keys = settings => + Lookup.fold((key, _, acc) => [key, ...acc], settings, []); + + let rec toJson = node => + switch ((node: t)) { + | Node(children) => + Json.Encode.obj( + children + |> Lookup.KeyedMap.to_seq + |> Seq.map(((key, value)) => (key, toJson(value))) + |> List.of_seq, + ) + | Leaf(value) => value + }; +}; + +// SCHEMA + +module Schema = { + type spec = { + path: Lookup.path, + default: Json.t, + }; + type t = Lookup.t(spec); + + let fromList = specs => + specs |> List.map(spec => (spec.path, spec)) |> Lookup.fromList; + + let union = (xs, ys) => + Lookup.union( + (path, _x, y) => { + Log.warnf(m => m("Encountered duplicate key: %s", Lookup.key(path))); + Some(y); + }, + xs, + ys, + ); + let unionMany = lookups => List.fold_left(union, Lookup.empty, lookups); + + let defaults = Lookup.map(spec => spec.default); + + // DSL + + module DSL = { + type setting('a) = { + spec, + get: resolver => 'a, + }; + type codec('a) = { + decode: Json.decoder('a), + encode: Json.encoder('a), + }; + + let bool = {decode: Json.Decode.bool, encode: Json.Encode.bool}; + let int = {decode: Json.Decode.int, encode: Json.Encode.int}; + let string = {decode: Json.Decode.string, encode: Json.Encode.string}; + let list = valueCodec => { + decode: Json.Decode.list(valueCodec.decode), + encode: Json.Encode.list(valueCodec.encode), + }; + + let custom = (~decode, ~encode) => {decode, encode}; + + let setting = (key, codec, ~default) => { + let path = Lookup.path(key); + + { + spec: { + path, + default: codec.encode(default), + }, + get: resolve => { + switch (resolve(path)) { + | Some(jsonValue) => + switch (Json.Decode.decode_value(codec.decode, jsonValue)) { + | Ok(value) => value + | Error(err) => + Log.errorf(m => + m( + "Failed to decode value for `%s`:\n\t%s", + key, + Json.Decode.string_of_error(err), + ) + ); + default; + } + | None => + Log.warnf(m => m("Missing default value for `%s`", key)); + default; + }; + }, + }; + }; + }; + + include DSL; +}; diff --git a/src/Types/Config.rei b/src/Types/Config.rei new file mode 100644 index 0000000..5ed7019 --- /dev/null +++ b/src/Types/Config.rei @@ -0,0 +1,75 @@ +type key; +type resolver = key => option(Json.t); + +let key: string => key; +let keyAsString: key => string; + +// SETTINGS + +module Settings: { + type t; + + let empty: t; + + let fromList: list((string, Json.t)) => t; + let fromJson: Json.t => t; + let fromFile: string => t; + + let get: (key, t) => option(Json.t); + + let union: (t, t) => t; + let unionMany: list(t) => t; + + /** Returns the set of changed keys with its new value, or `null` if removed */ + let diff: (t, t) => t; + + /** Returns the set of changed keys with the value `true`, intended for conversion to Json to mimic weird JavaScript semantics */ + let changed: (t, t) => t; + + let keys: t => list(key); + + let toJson: t => Json.t; +}; + +// SCHEMA + +module Schema: { + type t; + type spec; + + let fromList: list(spec) => t; + let union: (t, t) => t; + let unionMany: list(t) => t; + + let defaults: t => Settings.t; + + type codec('a); + type setting('a) = { + spec, + get: resolver => 'a, + }; + + // DSL + + module DSL: { + let bool: codec(bool); + let int: codec(int); + let string: codec(string); + let list: codec('a) => codec(list('a)); + + let custom: + (~decode: Json.decoder('a), ~encode: Json.encoder('a)) => codec('a); + + let setting: (string, codec('a), ~default: 'a) => setting('a); + }; + + let bool: codec(bool); + let int: codec(int); + let string: codec(string); + let list: codec('a) => codec(list('a)); + + let custom: + (~decode: Json.decoder('a), ~encode: Json.encoder('a)) => codec('a); + + let setting: (string, codec('a), ~default: 'a) => setting('a); +}; diff --git a/src/Types/IconTheme.re b/src/Types/IconTheme.re new file mode 100644 index 0000000..49bd1ab --- /dev/null +++ b/src/Types/IconTheme.re @@ -0,0 +1,170 @@ +/* + * IconTheme.re + * + * Typing / schema for icon themes + */ + +module FontSource = { + [@deriving (show({with_path: false}), yojson({strict: false}))] + type t = { + path: string, + /* format: string, */ + }; +}; + +[@deriving (show({with_path: false}), yojson({strict: false}))] +type fontSources = list(FontSource.t); + +module Font = { + [@deriving (show({with_path: false}), yojson({strict: false}))] + type t = { + id: string, + src: list(FontSource.t), + weight: string, + style: string, + size: string, + }; +}; + +[@deriving (show({with_path: false}), yojson({strict: false}))] +type fonts = list(Font.t); + +module IconDefinition = { + [@deriving (show({with_path: false}), yojson({strict: false}))] + type raw = { + fontCharacter: string, + fontColor: string, + }; + + type t = { + fontCharacter: int, + colorHex: string, + }; + + let parseId: string => int = + v => { + let len = String.length(v); + let sub = String.sub(v, 1, len - 1); + "0x" ++ sub |> int_of_string; + }; + let of_raw: raw => t = + raw => { + fontCharacter: parseId(raw.fontCharacter), + colorHex: raw.fontColor, + }; +}; + +type t = { + fonts: list(Font.t), + iconDefinitions: StringMap.t(IconDefinition.t), + file: string, + fileExtensions: StringMap.t(string), + fileNames: StringMap.t(string), + languageIds: StringMap.t(string), + /* TODO: Light mode */ +}; + +let normalizeExtension = s => + if (String.length(s) > 1 && Char.equal(s.[0], '.')) { + String.sub(s, 1, String.length(s) - 1); + } else { + s; + }; + +let getIconForFile: (t, string, string) => option(IconDefinition.t) = + (iconTheme: t, fileName: string, languageId: string) => { + let id = + switch (StringMap.find_opt(fileName, iconTheme.fileNames)) { + | Some(v) => v + | None => + switch ( + StringMap.find_opt( + normalizeExtension(Rench.Path.extname(fileName)), + iconTheme.fileExtensions, + ) + ) { + | Some(v) => v + | None => + switch (StringMap.find_opt(languageId, iconTheme.languageIds)) { + | Some(v) => v + | None => iconTheme.file + } + } + }; + + StringMap.find_opt(id, iconTheme.iconDefinitions); + }; + +let create = () => { + fonts: [], + iconDefinitions: StringMap.empty, + file: "", + fileExtensions: StringMap.empty, + fileNames: StringMap.empty, + languageIds: StringMap.empty, +}; + +let assocToStringMap = json => { + switch (json) { + | `Assoc(v) => + List.fold_left( + (prev, curr) => + switch (curr) { + | (s, `String(v)) => StringMap.add(s, v, prev) + | _ => prev + }, + StringMap.empty, + v, + ) + | _ => StringMap.empty + }; +}; + +let getOrEmpty = (v: result(list('a), 'b)) => { + switch (v) { + | Ok(v) => v + | Error(_) => [] + }; +}; + +let ofJson = (json: Yojson.Safe.t) => { + open Yojson.Safe.Util; + let fonts = json |> member("fonts") |> fonts_of_yojson |> getOrEmpty; + let icons = json |> member("iconDefinitions") |> to_assoc; + let file = json |> member("file") |> to_string; + let extensionsJson = json |> member("fileExtensions"); + let fileNamesJson = json |> member("fileNames"); + let languageIdsJson = json |> member("languageIds"); + + let toIconMap: + list((string, Yojson.Safe.t)) => StringMap.t(IconDefinition.t) = + icons => { + List.fold_left( + (prev, curr) => { + let (id, jsonItem) = curr; + switch (IconDefinition.raw_of_yojson(jsonItem)) { + | Ok(v) => + let icon = IconDefinition.of_raw(v); + StringMap.add(id, icon, prev); + | Error(_) => prev + }; + }, + StringMap.empty, + icons, + ); + }; + + let iconDefinitions = icons |> toIconMap; + let fileExtensions = assocToStringMap(extensionsJson); + let fileNames = assocToStringMap(fileNamesJson); + let languageIds = assocToStringMap(languageIdsJson); + + Some({ + fonts, + iconDefinitions, + file, + fileExtensions, + fileNames, + languageIds, + }); +}; diff --git a/src/Types/Menu.re b/src/Types/Menu.re new file mode 100644 index 0000000..ffa73d7 --- /dev/null +++ b/src/Types/Menu.re @@ -0,0 +1,98 @@ +module Log = (val Timber.Log.withNamespace("Exthost.Types.Menu")); + +// SCHEMA + +module Schema = { + [@deriving show] + type item = { + isVisibleWhen: WhenExpr.t, + group: option(string), + index: option(int), + command: string, + alt: option(string) // currently unused + }; + + type group = list(item); + + [@deriving show] + type definition = { + id: string, + items: list(item), + }; + + let extend = (id, groups) => {id, items: List.concat(groups)}; + + let group = (id, items) => + List.map(item => {...item, group: Some(id)}, items); + + let ungrouped = items => items; + + let item = (~index=?, ~alt=?, ~isVisibleWhen=WhenExpr.Value(True), command) => { + isVisibleWhen, + index, + command, + alt, + group: None, + }; +}; + +// MODEL + +[@deriving show] +type item = { + label: string, + category: option(string), + icon: [@opaque] option(IconTheme.IconDefinition.t), + isEnabledWhen: [@opaque] WhenExpr.t, + isVisibleWhen: [@opaque] WhenExpr.t, + group: option(string), + index: option(int), + command: string, +}; + +let fromSchemaItem = (commands, item: Schema.item) => + Command.Lookup.get(item.command, commands) + |> Option.map((command: Command.t(_)) => + { + label: command.title |> Option.value(~default=command.id), + category: command.category, + icon: command.icon, + isEnabledWhen: command.isEnabledWhen, + isVisibleWhen: item.isVisibleWhen, + group: item.group, + index: item.index, + command: command.id, + } + ); + +// LOOKUP + +module Lookup = { + type t = KeyedStringMap.t(list(item)); + + let fromList = entries => + entries + |> List.to_seq + |> Seq.map(((id, items)) => (KeyedStringMap.key(id), items)) + |> KeyedStringMap.of_seq; + + let fromSchema = (commands, definitions) => + definitions + |> List.map((definition: Schema.definition) => + ( + definition.id, + definition.items |> List.filter_map(fromSchemaItem(commands)), + ) + ) + |> fromList; + + let get = (id, lookup) => + KeyedStringMap.find_opt(KeyedStringMap.key(id), lookup) + |> Option.value(~default=[]); + + let union = (xs, ys) => + KeyedStringMap.union((_key, x, y) => Some(x @ y), xs, ys); + + let unionMany = lookups => + List.fold_left(union, KeyedStringMap.empty, lookups); +}; diff --git a/src/Types/Menu.rei b/src/Types/Menu.rei new file mode 100644 index 0000000..ea714b3 --- /dev/null +++ b/src/Types/Menu.rei @@ -0,0 +1,59 @@ +// SCHEMA + +module Schema: { + [@deriving show] + type item = { + isVisibleWhen: WhenExpr.t, + group: option(string), + index: option(int), + command: string, + alt: option(string) // currently unused + }; + + type group; + + [@deriving show] + type definition = { + id: string, + items: list(item), + }; + + let extend: (string, list(group)) => definition; + + let group: (string, list(item)) => group; + let ungrouped: list(item) => group; + + let item: + (~index: int=?, ~alt: string=?, ~isVisibleWhen: WhenExpr.t=?, string) => + item; +}; + +// MODEL + +[@deriving show] +type item = { + label: string, + category: option(string), + icon: [@opaque] option(IconTheme.IconDefinition.t), + isEnabledWhen: [@opaque] WhenExpr.t, + isVisibleWhen: [@opaque] WhenExpr.t, + group: option(string), + index: option(int), + command: string, +}; + +let fromSchemaItem: (Command.Lookup.t(_), Schema.item) => option(item); + +// LOOKUP + +module Lookup: { + type t; + + let fromList: list((string, list(item))) => t; + let fromSchema: (Command.Lookup.t(_), list(Schema.definition)) => t; + + let get: (string, t) => list(item); + + let union: (t, t) => t; + let unionMany: list(t) => t; +}; diff --git a/src/Types/dune b/src/Types/dune index 631916e..21f41b8 100644 --- a/src/Types/dune +++ b/src/Types/dune @@ -2,5 +2,5 @@ (name Types) (public_name vscode-exthost.types) (library_flags (-linkall)) - (libraries luv yojson decoders-yojson) + (libraries luv Rench yojson decoders-yojson vscode-exthost.core vscode-exthost.whenexpr) (preprocess (pps ppx_deriving.show ppx_deriving_yojson))) diff --git a/src/WhenExpr/ContextKeys.re b/src/WhenExpr/ContextKeys.re new file mode 100644 index 0000000..61611b8 --- /dev/null +++ b/src/WhenExpr/ContextKeys.re @@ -0,0 +1,90 @@ +module Log = (val Timber.Log.withNamespace("Oni2.Core.WhenExpr.ContextKeys")); + +module Lookup = Core.KeyedStringMap; + +module Value = { + [@deriving show({with_path: false})] + type t = + | String(string) + | True + | False; + + // Emulate JavaScript semantics + let asBool = + fun + | True => true + | False => false + | String("") => false + | String(_) => true; + + // Emulate JavaScript semantics + let asString = + fun + | True => "true" + | False => "false" + | String(str) => str; +}; + +module Schema = { + type entry('model) = { + key: string, + get: 'model => Value.t, + }; + + let define = (key, get) => {key, get}; + let bool = (key, get) => + define(key, model => get(model) ? Value.True : Value.False); + let string = (key, get) => define(key, model => Value.String(get(model))); + + type t('model) = Lookup.t(entry('model)); + + let fromList = entries => + entries + |> List.to_seq + |> Seq.map(entry => (Lookup.key(entry.key), entry)) + |> Lookup.of_seq; + + let union = (xs, ys) => + Lookup.union( + (key, _x, y) => { + Log.errorf(m => + m("Encountered duplicate context key: %s", Lookup.keyName(key)) + ); + Some(y); + }, + xs, + ys, + ); + let unionMany = lookups => List.fold_left(union, Lookup.empty, lookups); + + let map = f => + Lookup.map(entry => {...entry, get: model => entry.get(f(model))}); +}; + +type t = Lookup.t(Value.t); + +let fromList = entries => + entries + |> List.to_seq + |> Seq.map(((key, value)) => (Lookup.key(key), value)) + |> Lookup.of_seq; + +let fromSchema = (schema, model) => + Lookup.map(Schema.(entry => entry.get(model)), schema); + +let union = (xs, ys) => + Lookup.union( + (key, _x, y) => { + Log.errorf(m => + m("Encountered duplicate context key: %s", Lookup.keyName(key)) + ); + Some(y); + }, + xs, + ys, + ); +let unionMany = lookups => List.fold_left(union, Lookup.empty, lookups); + +let getValue = (lookup, key) => + Lookup.find_opt(Lookup.key(key), lookup) + |> Option.value(~default=Value.False); diff --git a/src/WhenExpr/WhenExpr.re b/src/WhenExpr/WhenExpr.re new file mode 100644 index 0000000..8ee091c --- /dev/null +++ b/src/WhenExpr/WhenExpr.re @@ -0,0 +1,137 @@ +module ContextKeys = ContextKeys; + +module Value = ContextKeys.Value; + +[@deriving show({with_path: false})] +type t = + | Defined(string) + | Eq(string, Value.t) + | Neq(string, Value.t) + | Regex(string, option(Re.re)) + | And(list(t)) + | Or(list(t)) + | Not(t) + | Value(Value.t); + +let evaluate = (expr, getValue) => { + let rec eval = + fun + | Defined(name) => getValue(name) |> Value.asBool + | Eq(key, value) => getValue(key) == value + | Neq(key, value) => getValue(key) != value + | Regex(_, None) => false + | Regex(key, Some(re)) => Re.execp(re, key |> getValue |> Value.asString) + | And(exprs) => List.for_all(eval, exprs) + | Or(exprs) => List.exists(eval, exprs) + | Not(expr) => !eval(expr) + | Value(value) => Value.asBool(value); + + eval(expr); +}; + +module Parse = { + // Translated relatively faithfully from + // https://github.com/microsoft/vscode/blob/e683dce828edccc6053bebab48a1954fb61f8e29/src/vs/platform/contextkey/common/contextkey.ts#L59 + + let deserializeValue = { + let quoted = Re.Posix.re("^'([^']*)'$") |> Re.compile; + + str => + switch (String.trim(str)) { + | "true" => Value.True + | "false" => Value.False + | str => + switch (Re.Group.get(Re.exec(quoted, str), 1)) { + | unquoted => Value.String(unquoted) + | exception Not_found => Value.String(str) + } + }; + }; + + let deserializeRegexValue = (~strict=false, str) => + switch (String.trim(str)) { + | "" when strict => failwith("missing regexp-value for =~-expression") + | "" => None + | str => + switch (String.index_opt(str, '/'), String.rindex_opt(str, '/')) { + | (Some(start), Some(stop)) when start == stop => + failwith("bad regexp-value '" ++ str ++ "', missing /-enclosure") + | (Some(start), Some(stop)) => + String.sub(str, start + 1, stop - start - 1) + |> Re.Pcre.re + |> Re.compile + |> Option.some + | (None, None) when strict => + failwith("bad regexp-value '" ++ str ++ "', missing /-enclosure") + | (None, None) => None + | _ => failwith("unreachable") + } + }; + + let deserializeOne = { + let eq = Re.str("==") |> Re.compile; + let neq = Re.str("!=") |> Re.compile; + let regex = Re.str("=~") |> Re.compile; + let not = Re.Pcre.re("^\\!\\s*") |> Re.compile; + + str => { + let str = String.trim(str); + + if (Re.execp(neq, str)) { + switch (Re.split(neq, str)) { + // This matches more than two "pieces", which should be a synatx error. + // But since vscode accepts it, so do we. + | [left, right, ..._] => + let key = String.trim(left); + switch (deserializeValue(right)) { + | True => Not(Defined(key)) + | False => Defined(key) + | value => Neq(key, value) + }; + | _ => failwith("unreachable") + }; + } else if (Re.execp(eq, str)) { + switch (Re.split(eq, str)) { + // This matches more than two "pieces", which should be a synatx error. + // But since vscode accepts it, so do we. + | [left, right, ..._] => + let key = String.trim(left); + switch (deserializeValue(right)) { + | True => Defined(key) + | False => Not(Defined(key)) + | value => Eq(key, value) + }; + | _ => failwith("unreachable") + }; + } else if (Re.execp(regex, str)) { + switch (Re.split(regex, str)) { + // This matches more than two "pieces", which should be a syntax error. + // But since vscode accepts it, so do we. + | [left, right, ..._] => + let key = String.trim(left); + let maybeRegex = deserializeRegexValue(right); + Regex(key, maybeRegex); + | _ => failwith("unreachable") + }; + } else if (Re.execp(not, str)) { + Not(Defined(Re.Str.string_after(str, 1) |> String.trim)); + } else { + Defined(str); + }; + }; + }; + + let deserializeAnd = { + let re = Re.str("&&") |> Re.compile; + + str => And(str |> Re.split(re) |> List.map(deserializeOne)); + }; + + let deserializeOr = { + let re = Re.str("||") |> Re.compile; + + str => Or(str |> Re.split(re) |> List.map(deserializeAnd)); + }; +}; + +let parse = Parse.deserializeOr; diff --git a/src/WhenExpr/WhenExpr.rei b/src/WhenExpr/WhenExpr.rei new file mode 100644 index 0000000..fdddc45 --- /dev/null +++ b/src/WhenExpr/WhenExpr.rei @@ -0,0 +1,52 @@ +module Value: { + [@deriving show] + type t = + | String(string) + | True + | False; + + let asBool: t => bool; + let asString: t => string; +}; + +module ContextKeys: { + module Schema: { + type entry('model); + type t('model); + + let define: (string, 'model => Value.t) => entry('model); + let bool: (string, 'model => bool) => entry('model); + let string: (string, 'model => string) => entry('model); + + let fromList: list(entry('model)) => t('model); + + let union: (t('model), t('model)) => t('model); + let unionMany: list(t('model)) => t('model); + + let map: ('a => 'b, t('b)) => t('a); + }; + + type t; + + let fromList: list((string, Value.t)) => t; + let fromSchema: (Schema.t('model), 'model) => t; + + let union: (t, t) => t; + let unionMany: list(t) => t; + + let getValue: (t, string) => Value.t; +}; + +[@deriving show] +type t = + | Defined(string) + | Eq(string, Value.t) + | Neq(string, Value.t) + | Regex(string, option(Re.re)) + | And(list(t)) + | Or(list(t)) + | Not(t) + | Value(Value.t); + +let evaluate: (t, string => Value.t) => bool; +let parse: string => t; diff --git a/src/WhenExpr/dune b/src/WhenExpr/dune new file mode 100644 index 0000000..2ef2343 --- /dev/null +++ b/src/WhenExpr/dune @@ -0,0 +1,5 @@ +(library + (name WhenExpr) + (public_name vscode-exthost.whenexpr) + (preprocess (pps ppx_deriving.show)) + (libraries re timber vscode-exthost.core)) diff --git a/src/dune b/src/dune index e230440..020126d 100644 --- a/src/dune +++ b/src/dune @@ -1,5 +1,5 @@ (library (name exthost) - (libraries timber vscode-exthost.extension vscode-exthost.types vscode-exthost.protocol luv yojson decoders-yojson) + (libraries timber vscode-exthost.extension vscode-exthost.types vscode-exthost.protocol vscode-exthost.whenexpr luv yojson decoders-yojson) (preprocess (pps ppx_deriving.show ppx_deriving_yojson)) (public_name vscode-exthost)) diff --git a/test/Types/IconThemeTests.re b/test/Types/IconThemeTests.re new file mode 100644 index 0000000..2cdd70d --- /dev/null +++ b/test/Types/IconThemeTests.re @@ -0,0 +1,104 @@ +open TestFramework; + +module IconTheme = Exthost.Types.IconTheme; + +let testTheme = {| + { + "information_for_contributors": [""], + "fonts": [ + { + "id": "seti", + "src": [ + { + "path": "./seti.ttf", + "format": "woff" + } + ], + "weight": "normal", + "style": "normal", + "size": "150%" + } + ], + "iconDefinitions": { + "_default": { + "fontCharacter": "\\E001", + "fontColor": "#ff0000" + }, + "_test_ext": { + "fontCharacter": "\\E002", + "fontColor": "#6d8086" + }, + "_test_file": { + "fontCharacter": "\\E090", + "fontColor": "#6d8086" + }, + "_test_language": { + "fontCharacter": "\\E091", + "fontColor": "#6d8086" + } + }, + "file": "_default", + "fileExtensions": { + "ext": "_test_ext" + }, + "fileNames": { + "file1": "_test_file" + }, + "languageIds": { + "language1": "_test_language" + }, + "version": "" +} +|}; + +let json: Yojson.Safe.t = Yojson.Safe.from_string(testTheme); + +describe("IconTheme", ({test, _}) => { + test("gets icon for matching filename", ({expect, _}) => { + let _iconTheme = IconTheme.ofJson(json) |> Option.get; + let icon: option(IconTheme.IconDefinition.t) = + IconTheme.getIconForFile(_iconTheme, "file1", "some-random-language"); + + switch (icon) { + | Some(v) => expect.int(v.fontCharacter).toBe(0xE090) + | None => expect.string("No icon found!").toEqual("") + }; + }); + + test("gets icon for matching extension", ({expect, _}) => { + let _iconTheme = IconTheme.ofJson(json) |> Option.get; + let icon: option(IconTheme.IconDefinition.t) = + IconTheme.getIconForFile( + _iconTheme, + "file1.ext", + "some-random-language", + ); + + switch (icon) { + | Some(v) => expect.int(v.fontCharacter).toBe(0xE002) + | None => expect.string("No icon found!").toEqual("") + }; + }); + + test("gets icon for matching extension", ({expect, _}) => { + let _iconTheme = IconTheme.ofJson(json) |> Option.get; + let icon: option(IconTheme.IconDefinition.t) = + IconTheme.getIconForFile(_iconTheme, "file1.rnd", "language1"); + + switch (icon) { + | Some(v) => expect.int(v.fontCharacter).toBe(0xE091) + | None => expect.string("No icon found!").toEqual("") + }; + }); + + test("falls back to default icon", ({expect, _}) => { + let _iconTheme = IconTheme.ofJson(json) |> Option.get; + let icon: option(IconTheme.IconDefinition.t) = + IconTheme.getIconForFile(_iconTheme, "file1.rnd", "unknown-language"); + + switch (icon) { + | Some(v) => expect.int(v.fontCharacter).toBe(0xE001) + | None => expect.string("No icon found!").toEqual("") + }; + }); +}); diff --git a/test/Types/TestFramework.re b/test/Types/TestFramework.re new file mode 100644 index 0000000..5b238f7 --- /dev/null +++ b/test/Types/TestFramework.re @@ -0,0 +1,7 @@ +include Rely.Make({ + let config = + Rely.TestFrameworkConfig.initialize({ + snapshotDir: "__snapshots__", + projectDir: "", + }); +}); diff --git a/test/Types/dune b/test/Types/dune new file mode 100644 index 0000000..4f0519d --- /dev/null +++ b/test/Types/dune @@ -0,0 +1,6 @@ +(library + (name ExtHostTypesTests) + (public_name vscode-exthost-test.types) + (library_flags (-linkall -g)) + (modules (:standard)) + (libraries vscode-exthost rely.lib console.lib)) diff --git a/test/WhenExpr/TestFramework.re b/test/WhenExpr/TestFramework.re new file mode 100644 index 0000000..5b238f7 --- /dev/null +++ b/test/WhenExpr/TestFramework.re @@ -0,0 +1,7 @@ +include Rely.Make({ + let config = + Rely.TestFrameworkConfig.initialize({ + snapshotDir: "__snapshots__", + projectDir: "", + }); +}); diff --git a/test/WhenExpr/WhenExprTests.re b/test/WhenExpr/WhenExprTests.re new file mode 100644 index 0000000..3e89b35 --- /dev/null +++ b/test/WhenExpr/WhenExprTests.re @@ -0,0 +1,65 @@ +open TestFramework; + +let re = (pattern, str) => { + let re = pattern |> Re.Pcre.re |> Re.compile; + Re.execp(re, str); +}; + +describe("WhenExpr", ({describe, _}) => { + describe("vscode tests", ({describe, _}) => { + // Translkated from https://github.com/microsoft/vscode/blob/7fc5d9150569247b3494eb3715a078bf7f8e9272/src/vs/platform/contextkey/test/common/contextkey.test.ts + // TODO: equals + // TODO: normalize + describe("evaluate", ({test, _}) => { + let context = Hashtbl.create(4); + Hashtbl.add(context, "a", WhenExpr.Value.True); + Hashtbl.add(context, "b", WhenExpr.Value.False); + Hashtbl.add(context, "c", WhenExpr.Value.String("5")); + Hashtbl.add(context, "d", WhenExpr.Value.String("d")); + + let getValue = name => + switch (Hashtbl.find_opt(context, name)) { + | Some(value) => value + | None => WhenExpr.Value.False + }; + + let testExpression = (expr, expected) => + test( + expr, + ({expect, _}) => { + let rules = WhenExpr.parse(expr); + // Console.log(WhenExpr.show(rules)); + expect.bool(WhenExpr.evaluate(rules, getValue)).toBe(expected); + }, + ); + let testBatch = (expr, value) => { + testExpression(expr, WhenExpr.Value.asBool(value)); + testExpression(expr ++ " == true", WhenExpr.Value.asBool(value)); + testExpression(expr ++ " != true", !WhenExpr.Value.asBool(value)); + testExpression(expr ++ " == false", !WhenExpr.Value.asBool(value)); + testExpression(expr ++ " != false", WhenExpr.Value.asBool(value)); + testExpression(expr ++ " == 5", value == String("5")); + testExpression(expr ++ " != 5", value != String("5")); + testExpression("!" ++ expr, !WhenExpr.Value.asBool(value)); + testExpression( + expr ++ " =~ /d.*/", + re("d.*", WhenExpr.Value.asString(value)), + ); + testExpression( + expr ++ " =~ /D/i", + re("D", WhenExpr.Value.asString(value)), + ); + }; + + testBatch("a", WhenExpr.Value.True); + testBatch("b", WhenExpr.Value.False); + testBatch("c", WhenExpr.Value.String("5")); + testBatch("d", WhenExpr.Value.String("d")); + testBatch("z", WhenExpr.Value.False); // `undefined` in vscode tests, but that's nonsense + + testExpression("a && !b", true && !false); + testExpression("a && b", true && false); + testExpression("a && !b && c == 5", true && !false && "5" == "5"); + }) + }) +}); diff --git a/test/WhenExpr/dune b/test/WhenExpr/dune new file mode 100644 index 0000000..3e08229 --- /dev/null +++ b/test/WhenExpr/dune @@ -0,0 +1,6 @@ +(library + (name ExtHostWhenExprTests) + (public_name vscode-exthost-test.whenexpr) + (library_flags (-linkall -g)) + (modules (:standard)) + (libraries vscode-exthost.whenexpr rely.lib console.lib)) diff --git a/test/bin/ExtHostUnitTestRunner.re b/test/bin/ExtHostUnitTestRunner.re index 0029827..59e5849 100644 --- a/test/bin/ExtHostUnitTestRunner.re +++ b/test/bin/ExtHostUnitTestRunner.re @@ -4,3 +4,5 @@ Timber.App.setLevel(Timber.Level.trace); ExtHostTest.TestFramework.cli(); ExtHostTransportTest.TestFramework.cli(); ExtHostExtensionTest.TestFramework.cli(); +ExtHostWhenExprTests.TestFramework.cli(); +ExtHostTypesTests.TestFramework.cli(); diff --git a/test/bin/dune b/test/bin/dune index 107121e..33b84bb 100644 --- a/test/bin/dune +++ b/test/bin/dune @@ -7,4 +7,6 @@ vscode-exthost-test vscode-exthost-test.transport vscode-exthost-test.extension + vscode-exthost-test.whenexpr + vscode-exthost-test.types ))