diff --git a/src/Components/Tabs.re b/src/Components/Tabs.re
index 726ec93401..8d8801a8bf 100644
--- a/src/Components/Tabs.re
+++ b/src/Components/Tabs.re
@@ -49,7 +49,7 @@ let schedulePostRender = f => postRenderQueue := [f, ...postRenderQueue^];
let component = React.Expert.component("Tabs");
let make =
(
- ~children as render: 'a => element,
+ ~children as render: (~isSelected: bool, ~index: int, 'a) => element,
~items: list('a),
~selectedIndex: option(int),
~style,
@@ -117,7 +117,12 @@ let make =
ref={r => setOuterRef(_ => Some(r))}
style=outerStyle>
- {List.map(render, items) |> React.listToElement}
+ {List.mapi(
+ index =>
+ render(~isSelected=Some(index) == selectedIndex, ~index),
+ items,
+ )
+ |> React.listToElement}
,
hooks,
diff --git a/src/Feature/Layout/Configuration.re b/src/Feature/Layout/Configuration.re
new file mode 100644
index 0000000000..d928254159
--- /dev/null
+++ b/src/Feature/Layout/Configuration.re
@@ -0,0 +1,59 @@
+open Oni_Core;
+open Config.Schema;
+
+module Codec: {
+ let showLayoutTabs: Config.Schema.codec([ | `always | `smart | `off]);
+ let layoutTabPosition: Config.Schema.codec([ | `top | `bottom]);
+} = {
+ let showLayoutTabs =
+ custom(
+ ~decode=
+ Json.Decode.(
+ string
+ |> map(
+ fun
+ | "always" => `always
+ | "smart" => `smart
+ | "off" => `off
+ | _ => `smart,
+ )
+ ),
+ ~encode=
+ Json.Encode.(
+ fun
+ | `always => string("always")
+ | `smart => string("smart")
+ | `off => string("off")
+ ),
+ );
+
+ let layoutTabPosition =
+ custom(
+ ~decode=
+ Json.Decode.(
+ string
+ |> map(
+ fun
+ | "top" => `top
+ | "bottom" => `bottom
+ | _ => `bottom,
+ )
+ ),
+ ~encode=
+ Json.Encode.(
+ fun
+ | `top => string("top")
+ | `bottom => string("bottom")
+ ),
+ );
+};
+
+let showLayoutTabs =
+ setting("oni.layout.showLayoutTabs", Codec.showLayoutTabs, ~default=`smart);
+
+let layoutTabPosition =
+ setting(
+ "oni.layout.layoutTabPosition",
+ Codec.layoutTabPosition,
+ ~default=`bottom,
+ );
diff --git a/src/Feature/Layout/EditorGroupView.re b/src/Feature/Layout/EditorGroupView.re
deleted file mode 100644
index ff7a60b448..0000000000
--- a/src/Feature/Layout/EditorGroupView.re
+++ /dev/null
@@ -1,95 +0,0 @@
-open Revery.UI;
-open Oni_Core;
-open Utility;
-open Oni_Components;
-open Model;
-open Msg;
-
-module Colors = Feature_Theme.Colors;
-
-module Styles = {
- open Style;
-
- let container = theme => [
- backgroundColor(Colors.Editor.background.from(theme)),
- color(Colors.foreground.from(theme)),
- position(`Absolute),
- top(0),
- left(0),
- right(0),
- bottom(0),
- ];
-
- let editorContainer = [flexGrow(1), flexDirection(`Column)];
-};
-
-module type ContentModel = {
- type t = Feature_Editor.Editor.t;
-
- let id: t => int;
- let title: t => string;
- let icon: t => option(IconTheme.IconDefinition.t);
- let isModified: t => bool;
-
- let render: (~isActive: bool, t) => element;
-};
-
-let make =
- (
- ~provider as module ContentModel: ContentModel,
- ~showTabs,
- ~uiFont,
- ~theme,
- ~isActive,
- ~model: Group.t,
- ~dispatch,
- (),
- ) => {
- let isSelected = item => ContentModel.id(item) == model.selectedId;
- let children = {
- let editorContainer =
- switch (List.find_opt(isSelected, model.editors)) {
- | Some(item) => ContentModel.render(~isActive, item)
- | None => React.empty
- };
-
- if (showTabs) {
- let editors = model.editors |> List.rev;
- let tabs =
-
- ...{item => {
-
- dispatch(GroupTabClicked(ContentModel.id(item)))
- }
- onClose={() =>
- dispatch(EditorCloseButtonClicked(ContentModel.id(item)))
- }
- />
- }}
- ;
-
- tabs editorContainer ;
- } else {
- editorContainer;
- };
- };
-
- let onMouseDown = _ => dispatch(GroupSelected(model.id));
-
- children ;
-};
diff --git a/src/Feature/Layout/EditorTab.re b/src/Feature/Layout/EditorTab.re
deleted file mode 100644
index f0c5ec5ad0..0000000000
--- a/src/Feature/Layout/EditorTab.re
+++ /dev/null
@@ -1,208 +0,0 @@
-open Revery.UI;
-open Oni_Core;
-
-module FontAwesome = Oni_Components.FontAwesome;
-module FontIcon = Oni_Components.FontIcon;
-module Sneakable = Feature_Sneak.View.Sneakable;
-
-module Theme = Feature_Theme;
-
-module Constants = {
- include Constants;
-
- let minWidth = 125;
-};
-
-module Colors = Theme.Colors.Tab;
-
-let proportion = factor =>
- float(Constants.minWidth) *. factor |> int_of_float;
-
-module Styles = {
- open Style;
-
- let container =
- (~isGroupFocused, ~isActive, ~isHovered, ~isModified, ~theme) => {
- let background = {
- let unhovered =
- switch (isActive, isGroupFocused) {
- | (false, _) => Colors.inactiveBackground
- | (true, false) => Colors.unfocusedActiveBackground
- | (true, true) => Colors.activeBackground
- };
-
- if (isHovered) {
- let color =
- isGroupFocused
- ? Colors.unfocusedHoverBackground : Colors.hoverBackground;
-
- color.tryFrom(theme) |> Option.value(~default=unhovered.from(theme));
- } else {
- unhovered.from(theme);
- };
- };
-
- let borderTop = {
- let color =
- if (isActive) {
- background;
- } else {
- let color =
- isGroupFocused
- ? Colors.activeBorderTop : Colors.unfocusedActiveBorderTop;
-
- color.tryFrom(theme) |> Option.value(~default=background);
- };
-
- borderTop(~color, ~width=2);
- };
-
- let borderBottom = {
- let color = {
- let unhovered =
- (
- switch (isActive, isGroupFocused, isModified) {
- | (false, _, false) => Colors.border
- | (false, false, true) => Colors.unfocusedInactiveModifiedBorder
- | (false, true, true) => Colors.inactiveModifiedBorder
- | (true, false, true) => Colors.unfocusedActiveModifiedBorder
- | (true, true, true) => Colors.activeModifiedBorder
- | (true, false, false) => Colors.unfocusedActiveBorder
- | (true, true, false) => Colors.activeBorder
- }
- ).
- tryFrom(
- theme,
- )
- |> Option.value(~default=background);
-
- if (isHovered) {
- let color =
- isGroupFocused ? Colors.unfocusedHoverBorder : Colors.hoverBorder;
-
- color.tryFrom(theme) |> Option.value(~default=unhovered);
- } else {
- unhovered;
- };
- };
-
- borderBottom(~color, ~width=1);
- };
-
- [
- overflow(`Hidden),
- paddingHorizontal(5),
- backgroundColor(background),
- borderTop,
- borderBottom,
- height(Constants.tabHeight),
- minWidth(Constants.minWidth),
- flexDirection(`Row),
- justifyContent(`Center),
- alignItems(`Center),
- ];
- };
-
- let text = (~isGroupFocused, ~isActive, ~theme) => {
- let foreground =
- switch (isActive, isGroupFocused) {
- | (false, false) => Colors.unfocusedInactiveForeground
- | (false, true) => Colors.inactiveForeground
- | (true, false) => Colors.unfocusedActiveForeground
- | (true, true) => Colors.activeForeground
- };
- [
- width(proportion(0.80) - 10),
- textOverflow(`Ellipsis),
- color(foreground.from(theme)),
- justifyContent(`Center),
- alignItems(`Center),
- ];
- };
-
- let icon = [
- width(32),
- height(Constants.tabHeight),
- alignItems(`Center),
- justifyContent(`Center),
- ];
-};
-
-let%component make =
- (
- ~title,
- ~isGroupFocused,
- ~isActive,
- ~isModified,
- ~onClick,
- ~onClose,
- ~theme: ColorTheme.Colors.t,
- ~uiFont: UiFont.t,
- ~icon,
- (),
- ) => {
- let%hook (isHovered, setHovered) = Hooks.state(false);
-
- let fileIconView =
- switch (icon) {
- | Some((icon: IconTheme.IconDefinition.t)) =>
-
- | None => React.empty
- };
-
- let onAnyClick = (evt: NodeEvents.mouseButtonEventParams) => {
- switch (evt.button) {
- | Revery.MouseButton.BUTTON_MIDDLE => onClose()
- | Revery.MouseButton.BUTTON_LEFT => onClick()
- | _ => ()
- };
- };
-
- setHovered(_ => true)}
- onMouseOut={_ => setHovered(_ => false)}
- style={Styles.container(
- ~isGroupFocused,
- ~isActive,
- ~isHovered,
- ~isModified,
- ~theme,
- )}>
-
- fileIconView
-
-
-
-
-
- ;
-};
diff --git a/src/Feature/Layout/Feature_Layout.re b/src/Feature/Layout/Feature_Layout.re
index cbd57ce735..0cec799cae 100644
--- a/src/Feature/Layout/Feature_Layout.re
+++ b/src/Feature/Layout/Feature_Layout.re
@@ -1,3 +1,5 @@
+open Oni_Core.Utility;
+
// MODEL
include Model;
@@ -12,74 +14,97 @@ type msg = Msg.t;
type outmsg =
| Nothing
| SplitAdded
- | RemoveLastBlocked
+ | RemoveLastWasBlocked
| Focus(panel);
open {
- let rotate = (direction, model) => {
- ...model,
- tree:
- Layout.rotate(direction, model.activeGroupId, activeTree(model)),
- };
-
- let resizeWindowByAxis = (direction, delta, model) => {
- ...model,
- tree:
- Layout.resizeWindowByAxis(
- direction,
- model.activeGroupId,
- delta,
- activeTree(model),
- ),
- };
-
- let resizeWindowByDirection = (direction, delta, model) => {
- ...model,
- tree:
- Layout.resizeWindowByDirection(
- direction,
- model.activeGroupId,
- delta,
- activeTree(model),
- ),
- };
-
- let resetWeights = model => {
- ...model,
- tree: Layout.resetWeights(activeTree(model)),
- uncommittedTree: `None,
- };
-
- let maximize = (~direction=?, model) => {
- ...model,
- uncommittedTree:
- `Maximized(
- Layout.maximize(
- ~direction?,
- model.activeGroupId,
- activeTree(model),
- ),
- ),
- };
+ let rotate = direction =>
+ updateActiveLayout(layout =>
+ {
+ ...layout,
+ tree:
+ Layout.rotate(
+ direction,
+ layout.activeGroupId,
+ activeTree(layout),
+ ),
+ }
+ );
+
+ let resizeWindowByAxis = (direction, delta) =>
+ updateActiveLayout(layout =>
+ {
+ ...layout,
+ tree:
+ Layout.resizeWindowByAxis(
+ direction,
+ layout.activeGroupId,
+ delta,
+ activeTree(layout),
+ ),
+ }
+ );
+
+ let resizeWindowByDirection = (direction, delta) =>
+ updateActiveLayout(layout =>
+ {
+ ...layout,
+ tree:
+ Layout.resizeWindowByDirection(
+ direction,
+ layout.activeGroupId,
+ delta,
+ activeTree(layout),
+ ),
+ }
+ );
+
+ let resetWeights =
+ updateActiveLayout(layout =>
+ {
+ ...layout,
+ tree: Layout.resetWeights(activeTree(layout)),
+ uncommittedTree: `None,
+ }
+ );
+
+ let maximize = (~direction=?) =>
+ updateActiveLayout(layout =>
+ {
+ ...layout,
+ uncommittedTree:
+ `Maximized(
+ Layout.maximize(
+ ~direction?,
+ layout.activeGroupId,
+ activeTree(layout),
+ ),
+ ),
+ }
+ );
};
let update = (~focus, model, msg) => {
switch (msg) {
- | SplitDragged({path, delta}) =>
- let model =
- switch (model.uncommittedTree) {
- | `Maximized(tree) => {...model, tree}
- | `Resizing(_)
- | `None => model
- };
- (
- {
- ...model,
- uncommittedTree:
- `Resizing(Layout.resizeSplit(~path, ~delta, model.tree)),
- },
+ | SplitDragged({path, delta}) => (
+ updateActiveLayout(
+ layout => {
+ let layout =
+ switch (layout.uncommittedTree) {
+ | `Maximized(tree) => {...layout, tree}
+ | `Resizing(_)
+ | `None => layout
+ };
+ {
+ ...layout,
+ uncommittedTree:
+ `Resizing(Layout.resizeSplit(~path, ~delta, layout.tree)),
+ };
+ },
+ model,
+ ),
Nothing,
- );
+ )
| DragComplete => (updateTree(Fun.id, model), Nothing)
@@ -88,15 +113,30 @@ let update = (~focus, model, msg) => {
Nothing,
)
- | GroupSelected(id) => ({...model, activeGroupId: id}, Focus(Center))
+ | GroupSelected(id) => (
+ updateActiveLayout(layout => {...layout, activeGroupId: id}, model),
+ Focus(Center),
+ )
| EditorCloseButtonClicked(id) =>
switch (removeEditor(id, model)) {
| Some(model) => (model, Nothing)
- | None => (model, RemoveLastBlocked)
+ | None => (model, RemoveLastWasBlocked)
+ }
+
+ | LayoutTabClicked(index) => (
+ {...model, activeLayoutIndex: index},
+ Nothing,
+ )
+
+ | LayoutCloseButtonClicked(index) =>
+ switch (removeLayoutTab(index, model)) {
+ | Some(model) => (model, Nothing)
+ | None => (model, RemoveLastWasBlocked)
}
| Command(NextEditor) => (nextEditor(model), Nothing)
+
| Command(PreviousEditor) => (previousEditor(model), Nothing)
| Command(SplitVertical) => (split(`Vertical, model), SplitAdded)
@@ -106,18 +146,26 @@ let update = (~focus, model, msg) => {
| Command(CloseActiveEditor) =>
switch (removeActiveEditor(model)) {
| Some(model) => (model, Nothing)
- | None => (model, RemoveLastBlocked)
+ | None => (model, RemoveLastWasBlocked)
}
| Command(MoveLeft) =>
switch (focus) {
| Some(Center) =>
+ let layout = model |> activeLayout;
let newActiveGroupId =
- model |> activeTree |> moveLeft(model.activeGroupId);
- if (newActiveGroupId == model.activeGroupId) {
+ layout |> activeTree |> moveLeft(layout.activeGroupId);
+
+ if (newActiveGroupId == layout.activeGroupId) {
(model, Focus(Left));
} else {
- ({...model, activeGroupId: newActiveGroupId}, Nothing);
+ (
+ updateActiveLayout(
+ layout => {...layout, activeGroupId: newActiveGroupId},
+ model,
+ ),
+ Nothing,
+ );
};
| Some(Left)
@@ -128,13 +176,27 @@ let update = (~focus, model, msg) => {
| Command(MoveRight) =>
switch (focus) {
| Some(Center) =>
- let newActiveGroupId =
- model |> activeTree |> moveRight(model.activeGroupId);
- ({...model, activeGroupId: newActiveGroupId}, Nothing);
+ let model =
+ updateActiveLayout(
+ layout => {
+ let newActiveGroupId =
+ layout |> activeTree |> moveRight(layout.activeGroupId);
+ {...layout, activeGroupId: newActiveGroupId};
+ },
+ model,
+ );
+ (model, Nothing);
| Some(Left) =>
- let newActiveGroupId = model |> activeTree |> Layout.leftmost;
- ({...model, activeGroupId: newActiveGroupId}, Focus(Center));
+ let model =
+ updateActiveLayout(
+ layout => {
+ let newActiveGroupId = layout |> activeTree |> Layout.leftmost;
+ {...layout, activeGroupId: newActiveGroupId};
+ },
+ model,
+ );
+ (model, Focus(Center));
| Some(Bottom)
| None => (model, Nothing)
@@ -143,13 +205,27 @@ let update = (~focus, model, msg) => {
| Command(MoveUp) =>
switch (focus) {
| Some(Center) =>
- let newActiveGroupId =
- model |> activeTree |> moveUp(model.activeGroupId);
- ({...model, activeGroupId: newActiveGroupId}, Nothing);
+ let model =
+ updateActiveLayout(
+ layout => {
+ let newActiveGroupId =
+ layout |> activeTree |> moveUp(layout.activeGroupId);
+ {...layout, activeGroupId: newActiveGroupId};
+ },
+ model,
+ );
+ (model, Nothing);
| Some(Bottom) =>
- let newActiveGroupId = model |> activeTree |> Layout.bottommost;
- ({...model, activeGroupId: newActiveGroupId}, Focus(Center));
+ let model =
+ updateActiveLayout(
+ layout => {
+ let newActiveGroupId = layout |> activeTree |> Layout.bottommost;
+ {...layout, activeGroupId: newActiveGroupId};
+ },
+ model,
+ );
+ (model, Focus(Center));
| Some(Left)
| None => (model, Nothing)
@@ -158,12 +234,20 @@ let update = (~focus, model, msg) => {
| Command(MoveDown) =>
switch (focus) {
| Some(Center) =>
+ let layout = model |> activeLayout;
let newActiveGroupId =
- model |> activeTree |> moveDown(model.activeGroupId);
- if (newActiveGroupId == model.activeGroupId) {
+ layout |> activeTree |> moveDown(layout.activeGroupId);
+
+ if (newActiveGroupId == layout.activeGroupId) {
(model, Focus(Bottom));
} else {
- ({...model, activeGroupId: newActiveGroupId}, Nothing);
+ (
+ updateActiveLayout(
+ layout => {...layout, activeGroupId: newActiveGroupId},
+ model,
+ ),
+ Nothing,
+ );
};
| Some(Left) => (model, Focus(Bottom))
@@ -319,8 +403,12 @@ let update = (~focus, model, msg) => {
| Command(ToggleMaximize) =>
let model =
- switch (model.uncommittedTree) {
- | `Maximized(_) => {...model, uncommittedTree: `None}
+ switch (activeLayout(model).uncommittedTree) {
+ | `Maximized(_) =>
+ updateActiveLayout(
+ layout => {...layout, uncommittedTree: `None},
+ model,
+ )
| _ =>
switch (focus) {
@@ -334,220 +422,38 @@ let update = (~focus, model, msg) => {
(model, Nothing);
| Command(ResetSizes) => (resetWeights(model), Nothing)
- };
-};
-// VIEW
-
-module type ContentModel = {
- type t = Feature_Editor.Editor.t;
-
- let id: t => int;
- let title: t => string;
- let icon: t => option(Oni_Core.IconTheme.IconDefinition.t);
- let isModified: t => bool;
-
- let render: (~isActive: bool, t) => Revery.UI.element;
-};
-
-module View = {
- module Local = {
- module Layout = Layout;
- };
- open Revery;
- open UI;
-
- module Constants = {
- let handleSize = 10;
- };
-
- module Styles = {
- open Style;
- let container = [flexGrow(1), flexDirection(`Row)];
+ | Command(AddLayout) => (addLayoutTab(model), Nothing)
- let verticalHandle = (node: Positioned.t(_)) => [
- cursor(MouseCursors.horizontalResize),
- position(`Absolute),
- left(node.meta.x + node.meta.width - Constants.handleSize / 2),
- top(node.meta.y),
- width(Constants.handleSize),
- height(node.meta.height),
- ];
-
- let horizontalHandle = (node: Positioned.t(_)) => [
- cursor(MouseCursors.verticalResize),
- position(`Absolute),
- left(node.meta.x),
- top(node.meta.y + node.meta.height - Constants.handleSize / 2),
- width(node.meta.width),
- height(Constants.handleSize),
- ];
- };
-
- let component = React.Expert.component("handleView");
- let handleView =
- (~direction, ~node: Positioned.t(_), ~onDrag, ~onDragComplete, ()) =>
- component(hooks => {
- let ((captureMouse, _state), hooks) =
- Hooks.mouseCapture(
- ~onMouseMove=
- ((originX, originY), evt) => {
- let delta =
- switch (direction) {
- | `Vertical => evt.mouseX -. originX
- | `Horizontal => evt.mouseY -. originY
- };
-
- onDrag(delta);
- Some((originX, originY));
- },
- ~onMouseUp=
- (_, _) => {
- onDragComplete();
- None;
- },
- (),
- hooks,
- );
-
- let onMouseDown = (evt: NodeEvents.mouseButtonEventParams) => {
- captureMouse((evt.mouseX, evt.mouseY));
- };
-
- (
- ,
- hooks,
- );
- });
-
- let rec nodeView =
- (
- ~theme,
- ~path=[],
- ~node: Positioned.t(_),
- ~renderWindow,
- ~dispatch,
- (),
- ) => {
- switch (node.kind) {
- | `Split(direction, children) =>
- let parent = node;
-
- let rec loop = (index, children) => {
- let path = [index, ...path];
-
- switch (children) {
- | [] => []
- | [node] => []
-
- | [node, ...[_, ..._] as rest] =>
- let onDrag = delta => {
- let total =
- direction == `Vertical ? parent.meta.width : parent.meta.height;
- dispatch(
- SplitDragged({
- path: List.rev(path),
- delta: delta /. float(total) // normalized
- }),
- );
- };
-
- let onDragComplete = () => dispatch(DragComplete);
-
- [
- ,
- ,
- ...loop(index + 1, rest),
- ];
- };
- };
+ | Command(PreviousLayout) => (
+ {
+ ...model,
+ activeLayoutIndex:
+ IndexEx.prevRollOver(
+ ~last=List.length(model.layouts) - 1,
+ model.activeLayoutIndex,
+ ),
+ },
+ Nothing,
+ )
- loop(0, children) |> React.listToElement;
-
- | `Window(id) =>
-
- {renderWindow(id)}
-
- };
+ | Command(NextLayout) => (
+ {
+ ...model,
+ activeLayoutIndex:
+ IndexEx.nextRollOver(
+ ~last=List.length(model.layouts) - 1,
+ model.activeLayoutIndex,
+ ),
+ },
+ Nothing,
+ )
};
+};
- let component = React.Expert.component("Feature_Layout.View");
- let make =
- (
- ~children as provider,
- ~model,
- ~isZenMode,
- ~showTabs,
- ~uiFont,
- ~theme,
- ~dispatch,
- (),
- ) =>
- component(hooks => {
- let ((maybeDimensions, setDimensions), hooks) =
- Hooks.state(None, hooks);
-
- let tree = activeTree(model);
-
- let children =
- switch (maybeDimensions) {
- | Some((width, height)) =>
- let positioned =
- isZenMode
- ? Positioned.fromWindow(
- 0,
- 0,
- width,
- height,
- model.activeGroupId,
- )
- : Positioned.fromLayout(0, 0, width, height, tree);
-
- let renderWindow = id =>
- switch (groupById(id, model)) {
- | Some(group) =>
-
- | None => React.empty
- };
+// VIEW
- ;
-
- | None => React.empty
- };
-
- (
-
- setDimensions(_ => Some((dim.width, dim.height)))
- }
- style=Styles.container>
- children
- ,
- hooks,
- );
- });
-};
+module View = View;
module Commands = {
open Feature_Commands.Schema;
@@ -773,8 +679,32 @@ module Commands = {
"workbench.action.evenEditorWidths",
Command(ResetSizes),
);
+
+ let addLayout =
+ define(
+ ~category="View",
+ ~title="Add Layout Tab",
+ "oni.layout.add",
+ Command(AddLayout),
+ );
+ let previousLayout =
+ define(
+ ~category="View",
+ ~title="Previous Layout Tab",
+ "oni.layout.previous",
+ Command(PreviousLayout),
+ );
+ let nextLayout =
+ define(
+ ~category="View",
+ ~title="Next Layout Tab",
+ "oni.layout.next",
+ Command(NextLayout),
+ );
};
+// CONTRIBUTIONS
+
module Contributions = {
let commands =
Commands.[
@@ -808,5 +738,11 @@ module Contributions = {
maximizeVertical,
toggleMaximize,
resetSizes,
+ addLayout,
+ previousLayout,
+ nextLayout,
];
+
+ let configuration =
+ Configuration.[showLayoutTabs.spec, layoutTabPosition.spec];
};
diff --git a/src/Feature/Layout/Feature_Layout.rei b/src/Feature/Layout/Feature_Layout.rei
index eb4c2c6e6b..26f0ad9e27 100644
--- a/src/Feature/Layout/Feature_Layout.rei
+++ b/src/Feature/Layout/Feature_Layout.rei
@@ -22,6 +22,8 @@ let activeEditor: model => Editor.t;
let openEditor: (Editor.t, model) => model;
let closeBuffer: (~force: bool, Vim.Types.buffer, model) => option(model);
+let addLayoutTab: model => model;
+
let map: (Editor.t => Editor.t, model) => model;
// UPDATE
@@ -32,26 +34,26 @@ type msg;
type outmsg =
| Nothing
| SplitAdded
- | RemoveLastBlocked
+ | RemoveLastWasBlocked
| Focus(panel);
let update: (~focus: option(panel), model, msg) => (model, outmsg);
// VIEW
-module type ContentModel = {
- type t = Editor.t;
+module View: {
+ open Revery.UI;
- let id: t => int;
- let title: t => string;
- let icon: t => option(IconTheme.IconDefinition.t);
- let isModified: t => bool;
+ module type ContentModel = {
+ type t = Editor.t;
- let render: (~isActive: bool, t) => Revery.UI.element;
-};
+ let id: t => int;
+ let title: t => string;
+ let icon: t => option(IconTheme.IconDefinition.t);
+ let isModified: t => bool;
-module View: {
- open Revery.UI;
+ let render: (~isActive: bool, t) => Revery.UI.element;
+ };
let make:
(
@@ -59,6 +61,7 @@ module View: {
~model: model,
~isZenMode: bool,
~showTabs: bool,
+ ~config: Config.resolver,
~uiFont: UiFont.t,
~theme: ColorTheme.Colors.t,
~dispatch: msg => unit,
@@ -105,8 +108,15 @@ module Commands: {
let maximizeVertical: Command.t(msg);
let toggleMaximize: Command.t(msg);
let resetSizes: Command.t(msg);
+
+ let addLayout: Command.t(msg);
+ let previousLayout: Command.t(msg);
+ let nextLayout: Command.t(msg);
};
// CONTRIBUTIONS
-module Contributions: {let commands: list(Command.t(msg));};
+module Contributions: {
+ let commands: list(Command.t(msg));
+ let configuration: list(Config.Schema.spec);
+};
diff --git a/src/Feature/Layout/Model.re b/src/Feature/Layout/Model.re
index 615b62a452..ad2376075b 100644
--- a/src/Feature/Layout/Model.re
+++ b/src/Feature/Layout/Model.re
@@ -121,7 +121,7 @@ type panel =
| Center
| Bottom;
-type model = {
+type layout = {
tree: Layout.t(int),
uncommittedTree: [
| `Resizing(Layout.t(int))
@@ -132,57 +132,80 @@ type model = {
activeGroupId: int,
};
+let activeTree = layout =>
+ switch (layout.uncommittedTree) {
+ | `Resizing(tree)
+ | `Maximized(tree) => tree
+ | `None => layout.tree
+ };
+
+type model = {
+ layouts: list(layout),
+ activeLayoutIndex: int,
+};
+
let initial = editors => {
let initialGroup = Group.create(editors);
- {
+ let initialLayout = {
tree: Layout.singleton(initialGroup.id),
uncommittedTree: `None,
groups: [initialGroup],
activeGroupId: initialGroup.id,
};
+
+ {layouts: [initialLayout], activeLayoutIndex: 0};
};
-let groupById = (id, model) =>
- List.find_opt((group: Group.t) => group.id == id, model.groups);
+let groupById = (id, layout) =>
+ List.find_opt((group: Group.t) => group.id == id, layout.groups);
+
+let activeLayout = model => List.nth(model.layouts, model.activeLayoutIndex);
let activeGroup = model =>
- groupById(model.activeGroupId, model) |> Option.get;
+ model
+ |> activeLayout
+ |> (layout => groupById(layout.activeGroupId, layout) |> Option.get);
let activeEditor = model => model |> activeGroup |> Group.selected;
-let activeTree = model =>
- switch (model.uncommittedTree) {
- | `Resizing(tree)
- | `Maximized(tree) => tree
- | `None => model.tree
- };
-
-let updateTree = (f, model) => {
+let updateActiveLayout = (f, model) => {
...model,
- tree: f(activeTree(model)),
- uncommittedTree: `None,
-};
-
-let updateActiveGroup = (f, model) => {
- ...model,
- groups:
- List.map(
- (group: Group.t) => group.id == model.activeGroupId ? f(group) : group,
- model.groups,
+ layouts:
+ List.mapi(
+ (i, layout) => i == model.activeLayoutIndex ? f(layout) : layout,
+ model.layouts,
),
};
-let windows = model => Layout.windows(activeTree(model));
+let updateTree = f =>
+ updateActiveLayout(layout =>
+ {...layout, tree: f(activeTree(layout)), uncommittedTree: `None}
+ );
+
+let updateActiveGroup = f =>
+ updateActiveLayout(layout =>
+ {
+ ...layout,
+ groups:
+ List.map(
+ (group: Group.t) =>
+ group.id == layout.activeGroupId ? f(group) : group,
+ layout.groups,
+ ),
+ }
+ );
+
+let windows = model => Layout.windows(model |> activeLayout |> activeTree);
let visibleEditors = model =>
model
|> windows
- |> List.filter_map(id => groupById(id, model))
+ |> List.filter_map(id => model |> activeLayout |> groupById(id))
|> List.map(Group.selected);
let editorById = (id, model) =>
- Base.List.find_map(model.groups, ~f=group =>
+ Base.List.find_map(activeLayout(model).groups, ~f=group =>
List.find_opt(editor => Editor.getId(editor) == id, group.editors)
);
@@ -196,18 +219,22 @@ let split = (direction, model) => {
let activeEditor = activeEditor(model);
let newGroup = Group.create([Editor.copy(activeEditor)]);
- {
- groups: [newGroup, ...model.groups],
- activeGroupId: newGroup.id,
- tree:
- Layout.insertWindow(
- `After(model.activeGroupId),
- direction,
- newGroup.id,
- activeTree(model),
- ),
- uncommittedTree: `None,
- };
+ updateActiveLayout(
+ layout =>
+ {
+ groups: [newGroup, ...layout.groups],
+ activeGroupId: newGroup.id,
+ tree:
+ Layout.insertWindow(
+ `After(layout.activeGroupId),
+ direction,
+ newGroup.id,
+ activeTree(layout),
+ ),
+ uncommittedTree: `None,
+ },
+ model,
+ );
};
let move = (focus, dirX, dirY, layout) => {
@@ -222,44 +249,76 @@ let moveRight = current => move(current, 1, 0);
let moveUp = current => move(current, 0, -1);
let moveDown = current => move(current, 0, 1);
-let nextEditor = model => updateActiveGroup(Group.nextEditor, model);
+let nextEditor = updateActiveGroup(Group.nextEditor);
-let previousEditor = model => updateActiveGroup(Group.previousEditor, model);
+let previousEditor = updateActiveGroup(Group.previousEditor);
-let openEditor = (editor, model) =>
- updateActiveGroup(Group.openEditor(editor), model);
+let openEditor = editor => updateActiveGroup(Group.openEditor(editor));
-let removeEditor = (editorId, model) => {
- let groups =
- List.filter_map(
- (group: Group.t) =>
- group.id == model.activeGroupId
- ? Group.removeEditor(editorId, group) : Some(group),
- model.groups,
- );
+let removeLayoutTab = (index, model) => {
+ let left = Base.List.take(model.layouts, index);
+ let right = Base.List.drop(model.layouts, index + 1);
+ let layouts = left @ right;
- if (groups == []) {
- None; // Group was removed, no groups left. Abort! Abort!
- } else if (List.length(groups) != List.length(model.groups)) {
- // Group was removed, remove from tree and make another active
-
- let tree = Layout.removeWindow(model.activeGroupId, activeTree(model));
-
- let activeGroupId =
- switch (
- ListEx.findIndex(
- (g: Group.t) => g.id == model.activeGroupId,
- model.groups,
- )
- ) {
- | Some(0) => List.hd(groups).id
- | Some(i) => List.nth(groups, i - 1).id
- | None => model.activeGroupId
- };
-
- Some({...model, tree, groups, activeGroupId});
+ if (layouts == []) {
+ None;
} else {
- Some({...model, groups});
+ Some({
+ layouts,
+ activeLayoutIndex:
+ min(model.activeLayoutIndex, List.length(layouts) - 1),
+ });
+ };
+};
+
+let removeEditor = (editorId, model) => {
+ let removeFromLayout = layout => {
+ let groups =
+ List.filter_map(
+ (group: Group.t) =>
+ group.id == layout.activeGroupId
+ ? Group.removeEditor(editorId, group) : Some(group),
+ layout.groups,
+ );
+
+ if (groups == []) {
+ None; // Group was removed, no groups left. Abort! Abort!
+ } else if (List.length(groups) != List.length(layout.groups)) {
+ // Group was removed, remove from tree and make another active
+
+ let tree =
+ Layout.removeWindow(layout.activeGroupId, activeTree(layout));
+
+ let activeGroupId =
+ switch (
+ ListEx.findIndex(
+ (g: Group.t) => g.id == layout.activeGroupId,
+ layout.groups,
+ )
+ ) {
+ | Some(0) => List.hd(groups).id
+ | Some(i) => List.nth(groups, i - 1).id
+ | None => layout.activeGroupId
+ };
+
+ Some({...layout, tree, groups, activeGroupId});
+ } else {
+ Some({...layout, groups});
+ };
+ };
+
+ switch (removeFromLayout(activeLayout(model))) {
+ | Some(newLayout) =>
+ Some({
+ ...model,
+ layouts:
+ List.mapi(
+ (i, layout) => i == model.activeLayoutIndex ? newLayout : layout,
+ model.layouts,
+ ),
+ })
+
+ | None => removeLayoutTab(model.activeLayoutIndex, model)
};
};
@@ -281,7 +340,31 @@ let closeBuffer = (~force, buffer, model) => {
};
};
+let addLayoutTab = model => {
+ let newEditor = activeEditor(model) |> Editor.copy;
+ let newGroup = Group.create([newEditor]);
+
+ let newLayout = {
+ tree: Layout.singleton(newGroup.id),
+ uncommittedTree: `None,
+ groups: [newGroup],
+ activeGroupId: newGroup.id,
+ };
+
+ let left = Base.List.take(model.layouts, model.activeLayoutIndex + 1);
+ let right = Base.List.drop(model.layouts, model.activeLayoutIndex + 1);
+
+ {
+ layouts: left @ [newLayout] @ right,
+ activeLayoutIndex: model.activeLayoutIndex + 1,
+ };
+};
+
let map = (f, model) => {
...model,
- groups: List.map(Group.map(f), model.groups),
+ layouts:
+ List.map(
+ layout => {...layout, groups: List.map(Group.map(f), layout.groups)},
+ model.layouts,
+ ),
};
diff --git a/src/Feature/Layout/Msg.re b/src/Feature/Layout/Msg.re
index 6b04d00bce..79a5c5a093 100644
--- a/src/Feature/Layout/Msg.re
+++ b/src/Feature/Layout/Msg.re
@@ -1,7 +1,7 @@
[@deriving show({with_path: false})]
type command =
- | NextEditor
| PreviousEditor
+ | NextEditor
| SplitVertical
| SplitHorizontal
| CloseActiveEditor
@@ -23,7 +23,10 @@ type command =
| MaximizeHorizontal
| MaximizeVertical
| ToggleMaximize
- | ResetSizes;
+ | ResetSizes
+ | AddLayout
+ | PreviousLayout
+ | NextLayout;
[@deriving show({with_path: false})]
type t =
@@ -35,4 +38,6 @@ type t =
| GroupTabClicked(int)
| GroupSelected(int)
| EditorCloseButtonClicked(int)
+ | LayoutTabClicked(int)
+ | LayoutCloseButtonClicked(int)
| Command(command);
diff --git a/src/Feature/Layout/View.re b/src/Feature/Layout/View.re
new file mode 100644
index 0000000000..f2984a3290
--- /dev/null
+++ b/src/Feature/Layout/View.re
@@ -0,0 +1,616 @@
+module Local = {
+ module Layout = Layout;
+ module Configuration = Configuration;
+};
+open Revery;
+open Revery.UI;
+open Oni_Core;
+open Utility;
+open Oni_Components;
+open Model;
+open Msg;
+
+module Colors = Feature_Theme.Colors;
+
+module type ContentModel = {
+ type t = Feature_Editor.Editor.t;
+
+ let id: t => int;
+ let title: t => string;
+ let icon: t => option(Oni_Core.IconTheme.IconDefinition.t);
+ let isModified: t => bool;
+
+ let render: (~isActive: bool, t) => Revery.UI.element;
+};
+
+module Tab = {
+ module FontAwesome = Oni_Components.FontAwesome;
+ module FontIcon = Oni_Components.FontIcon;
+ module Sneakable = Feature_Sneak.View.Sneakable;
+
+ module Theme = Feature_Theme;
+
+ module Constants = {
+ include Constants;
+
+ let minWidth = 125;
+ };
+
+ module Colors = Theme.Colors.Tab;
+
+ let proportion = factor =>
+ float(Constants.minWidth) *. factor |> int_of_float;
+
+ module Styles = {
+ open Style;
+
+ let container =
+ (~isGroupFocused, ~isActive, ~isHovered, ~isModified, ~theme) => {
+ let background = {
+ let unhovered =
+ switch (isActive, isGroupFocused) {
+ | (false, _) => Colors.inactiveBackground
+ | (true, false) => Colors.unfocusedActiveBackground
+ | (true, true) => Colors.activeBackground
+ };
+
+ if (isHovered) {
+ let color =
+ isGroupFocused
+ ? Colors.unfocusedHoverBackground : Colors.hoverBackground;
+
+ color.tryFrom(theme)
+ |> Option.value(~default=unhovered.from(theme));
+ } else {
+ unhovered.from(theme);
+ };
+ };
+
+ let borderTop = {
+ let color =
+ if (isActive) {
+ background;
+ } else {
+ let color =
+ isGroupFocused
+ ? Colors.activeBorderTop : Colors.unfocusedActiveBorderTop;
+
+ color.tryFrom(theme) |> Option.value(~default=background);
+ };
+
+ borderTop(~color, ~width=2);
+ };
+
+ let borderBottom = {
+ let color = {
+ let unhovered =
+ (
+ switch (isActive, isGroupFocused, isModified) {
+ | (false, _, false) => Colors.border
+ | (false, false, true) => Colors.unfocusedInactiveModifiedBorder
+ | (false, true, true) => Colors.inactiveModifiedBorder
+ | (true, false, true) => Colors.unfocusedActiveModifiedBorder
+ | (true, true, true) => Colors.activeModifiedBorder
+ | (true, false, false) => Colors.unfocusedActiveBorder
+ | (true, true, false) => Colors.activeBorder
+ }
+ ).
+ tryFrom(
+ theme,
+ )
+ |> Option.value(~default=background);
+
+ if (isHovered) {
+ let color =
+ isGroupFocused
+ ? Colors.unfocusedHoverBorder : Colors.hoverBorder;
+
+ color.tryFrom(theme) |> Option.value(~default=unhovered);
+ } else {
+ unhovered;
+ };
+ };
+
+ borderBottom(~color, ~width=1);
+ };
+
+ [
+ overflow(`Hidden),
+ paddingHorizontal(5),
+ backgroundColor(background),
+ borderTop,
+ borderBottom,
+ height(Constants.tabHeight),
+ minWidth(Constants.minWidth),
+ flexDirection(`Row),
+ justifyContent(`Center),
+ alignItems(`Center),
+ ];
+ };
+
+ let text = (~isGroupFocused, ~isActive, ~theme) => {
+ let foreground =
+ switch (isActive, isGroupFocused) {
+ | (false, false) => Colors.unfocusedInactiveForeground
+ | (false, true) => Colors.inactiveForeground
+ | (true, false) => Colors.unfocusedActiveForeground
+ | (true, true) => Colors.activeForeground
+ };
+ [
+ width(proportion(0.80) - 10),
+ textOverflow(`Ellipsis),
+ color(foreground.from(theme)),
+ justifyContent(`Center),
+ alignItems(`Center),
+ ];
+ };
+
+ let icon = [
+ width(32),
+ height(Constants.tabHeight),
+ alignItems(`Center),
+ justifyContent(`Center),
+ ];
+ };
+
+ let%component make =
+ (
+ ~title,
+ ~isGroupFocused,
+ ~isActive,
+ ~isModified,
+ ~onClick,
+ ~onClose,
+ ~theme: ColorTheme.Colors.t,
+ ~uiFont: UiFont.t,
+ ~icon,
+ (),
+ ) => {
+ let%hook (isHovered, setHovered) = Hooks.state(false);
+
+ let fileIconView =
+ switch (icon) {
+ | Some((icon: IconTheme.IconDefinition.t)) =>
+
+ | None => React.empty
+ };
+
+ let onAnyClick = (evt: NodeEvents.mouseButtonEventParams) => {
+ switch (evt.button) {
+ | Revery.MouseButton.BUTTON_MIDDLE => onClose()
+ | Revery.MouseButton.BUTTON_LEFT => onClick()
+ | _ => ()
+ };
+ };
+
+ setHovered(_ => true)}
+ onMouseOut={_ => setHovered(_ => false)}
+ style={Styles.container(
+ ~isGroupFocused,
+ ~isActive,
+ ~isHovered,
+ ~isModified,
+ ~theme,
+ )}>
+
+ fileIconView
+
+
+
+
+
+ ;
+ };
+};
+
+module EditorGroupView = {
+ module Styles = {
+ open Style;
+
+ let container = theme => [
+ backgroundColor(Colors.Editor.background.from(theme)),
+ color(Colors.foreground.from(theme)),
+ position(`Absolute),
+ top(0),
+ left(0),
+ right(0),
+ bottom(0),
+ ];
+
+ let editorContainer = [flexGrow(1), flexDirection(`Column)];
+ };
+
+ module type ContentModel = {
+ type t = Feature_Editor.Editor.t;
+
+ let id: t => int;
+ let title: t => string;
+ let icon: t => option(IconTheme.IconDefinition.t);
+ let isModified: t => bool;
+
+ let render: (~isActive: bool, t) => element;
+ };
+
+ let make =
+ (
+ ~provider as module ContentModel: ContentModel,
+ ~showTabs,
+ ~uiFont,
+ ~theme,
+ ~isActive,
+ ~model: Group.t,
+ ~dispatch,
+ (),
+ ) => {
+ let isSelected = item => ContentModel.id(item) == model.selectedId;
+ let children = {
+ let editorContainer =
+ switch (List.find_opt(isSelected, model.editors)) {
+ | Some(item) => ContentModel.render(~isActive, item)
+ | None => React.empty
+ };
+
+ if (showTabs) {
+ let editors = model.editors |> List.rev;
+ let tabs =
+
+ ...{(~isSelected, ~index as _, item) => {
+
+ dispatch(GroupTabClicked(ContentModel.id(item)))
+ }
+ onClose={() =>
+ dispatch(EditorCloseButtonClicked(ContentModel.id(item)))
+ }
+ />
+ }}
+ ;
+
+ tabs editorContainer ;
+ } else {
+ editorContainer;
+ };
+ };
+
+ let onMouseDown = _ => dispatch(GroupSelected(model.id));
+
+ children ;
+ };
+};
+
+module Layout = {
+ module Constants = {
+ let handleSize = 10;
+ };
+
+ module Styles = {
+ open Style;
+
+ let container = [flexGrow(1), flexDirection(`Row)];
+
+ let verticalHandle = (node: Positioned.t(_)) => [
+ cursor(MouseCursors.horizontalResize),
+ position(`Absolute),
+ left(node.meta.x + node.meta.width - Constants.handleSize / 2),
+ top(node.meta.y),
+ width(Constants.handleSize),
+ height(node.meta.height),
+ ];
+
+ let horizontalHandle = (node: Positioned.t(_)) => [
+ cursor(MouseCursors.verticalResize),
+ position(`Absolute),
+ left(node.meta.x),
+ top(node.meta.y + node.meta.height - Constants.handleSize / 2),
+ width(node.meta.width),
+ height(Constants.handleSize),
+ ];
+ };
+
+ let component = React.Expert.component("handleView");
+ let handleView =
+ (~direction, ~node: Positioned.t(_), ~onDrag, ~onDragComplete, ()) =>
+ component(hooks => {
+ let ((captureMouse, _state), hooks) =
+ Hooks.mouseCapture(
+ ~onMouseMove=
+ ((originX, originY), evt) => {
+ let delta =
+ switch (direction) {
+ | `Vertical => evt.mouseX -. originX
+ | `Horizontal => evt.mouseY -. originY
+ };
+
+ onDrag(delta);
+ Some((originX, originY));
+ },
+ ~onMouseUp=
+ (_, _) => {
+ onDragComplete();
+ None;
+ },
+ (),
+ hooks,
+ );
+
+ let onMouseDown = (evt: NodeEvents.mouseButtonEventParams) => {
+ captureMouse((evt.mouseX, evt.mouseY));
+ };
+
+ (
+ ,
+ hooks,
+ );
+ });
+
+ let rec nodeView =
+ (
+ ~theme,
+ ~path=[],
+ ~node: Positioned.t(_),
+ ~renderWindow,
+ ~dispatch,
+ (),
+ ) => {
+ switch (node.kind) {
+ | `Split(direction, children) =>
+ let parent = node;
+
+ let rec loop = (index, children) => {
+ let path = [index, ...path];
+
+ switch (children) {
+ | [] => []
+ | [node] => []
+
+ | [node, ...[_, ..._] as rest] =>
+ let onDrag = delta => {
+ let total =
+ direction == `Vertical ? parent.meta.width : parent.meta.height;
+ dispatch(
+ SplitDragged({
+ path: List.rev(path),
+ delta: delta /. float(total) // normalized
+ }),
+ );
+ };
+
+ let onDragComplete = () => dispatch(DragComplete);
+
+ [
+ ,
+ ,
+ ...loop(index + 1, rest),
+ ];
+ };
+ };
+
+ loop(0, children) |> React.listToElement;
+
+ | `Window(id) =>
+
+ {renderWindow(id)}
+
+ };
+ };
+
+ let component = React.Expert.component("Feature_Layout.View");
+ let make =
+ (
+ ~provider,
+ ~model as layout,
+ ~isZenMode,
+ ~showTabs,
+ ~uiFont,
+ ~theme,
+ ~dispatch,
+ (),
+ ) =>
+ component(hooks => {
+ let ((maybeDimensions, setDimensions), hooks) =
+ Hooks.state(None, hooks);
+
+ let tree = activeTree(layout);
+
+ let children =
+ switch (maybeDimensions) {
+ | Some((width, height)) =>
+ let positioned =
+ isZenMode
+ ? Positioned.fromWindow(
+ 0,
+ 0,
+ width,
+ height,
+ layout.activeGroupId,
+ )
+ : Positioned.fromLayout(0, 0, width, height, tree);
+
+ let renderWindow = id =>
+ switch (groupById(id, layout)) {
+ | Some(group) =>
+
+ | None => React.empty
+ };
+
+ ;
+
+ | None => React.empty
+ };
+
+ (
+
+ setDimensions(_ => Some((dim.width, dim.height)))
+ }
+ style=Styles.container>
+ children
+ ,
+ hooks,
+ );
+ });
+};
+
+module Styles = {
+ open Style;
+
+ let container = theme => [
+ backgroundColor(Colors.Editor.background.from(theme)),
+ color(Colors.foreground.from(theme)),
+ position(`Absolute),
+ top(0),
+ left(0),
+ right(0),
+ bottom(0),
+ ];
+
+ let editorContainer = [flexGrow(1), flexDirection(`Column)];
+};
+
+let make =
+ (
+ ~children as provider,
+ ~model,
+ ~isZenMode,
+ ~showTabs,
+ ~config,
+ ~uiFont,
+ ~theme,
+ ~dispatch,
+ (),
+ ) => {
+ let activeLayout =
+ ;
+
+ let showLayoutTabs =
+ switch (Local.Configuration.showLayoutTabs.get(config)) {
+ | `always => true
+ | `smart when List.length(model.layouts) > 1 => true
+ | `smart
+ | `off => false
+ };
+
+ if (showLayoutTabs && !isZenMode) {
+ module ContentModel = (val provider);
+
+ let tabs =
+
+ ...{(~isSelected, ~index, layout) => {
+ let groupCount = List.length(layout.groups);
+ let activeGroup =
+ List.find(
+ (group: Group.t) => group.id == layout.activeGroupId,
+ layout.groups,
+ );
+ let activeEditor = Group.selected(activeGroup);
+ let title =
+ Printf.sprintf(
+ "%i - %s",
+ groupCount,
+ ContentModel.title(activeEditor),
+ );
+ let isModified =
+ List.exists(ContentModel.isModified, activeGroup.editors);
+
+ dispatch(LayoutTabClicked(index))}
+ onClose={() => dispatch(LayoutCloseButtonClicked(index))}
+ />;
+ }}
+ ;
+
+ switch (Local.Configuration.layoutTabPosition.get(config)) {
+ | `top => tabs activeLayout
+ | `bottom => activeLayout tabs
+ };
+ } else {
+ activeLayout;
+ };
+};
diff --git a/src/Model/Actions.re b/src/Model/Actions.re
index 6f8054cb11..f87523b4f3 100644
--- a/src/Model/Actions.re
+++ b/src/Model/Actions.re
@@ -142,6 +142,9 @@ type t =
option([ | `Horizontal | `Vertical]),
option(Location.t),
)
+ | OpenFileInNewLayout(string)
+ | BufferOpened(string, option(Location.t), int)
+ | BufferOpenedForLayout(int)
| OpenConfigFile(string)
| QuitBuffer([@opaque] Vim.Buffer.t, bool)
| Quit(bool)
diff --git a/src/Model/State.re b/src/Model/State.re
index 3d6aee7ea5..2db4a125fa 100644
--- a/src/Model/State.re
+++ b/src/Model/State.re
@@ -113,6 +113,7 @@ let initial =
Feature_Editor.Contributions.configuration,
Feature_Syntax.Contributions.configuration,
Feature_Terminal.Contributions.configuration,
+ Feature_Layout.Contributions.configuration,
],
),
configuration: Configuration.default,
diff --git a/src/Store/Features.re b/src/Store/Features.re
index 2741b0df8c..b1173d805d 100644
--- a/src/Store/Features.re
+++ b/src/Store/Features.re
@@ -285,7 +285,7 @@ let update =
| SplitAdded => ({...state, zenMode: false}, Effect.none)
- | RemoveLastBlocked => (state, Internal.quitEffect)
+ | RemoveLastWasBlocked => (state, Internal.quitEffect)
| Nothing => (state, Effect.none)
};
diff --git a/src/Store/VimStoreConnector.re b/src/Store/VimStoreConnector.re
index 2754a878f7..b6d29c95da 100644
--- a/src/Store/VimStoreConnector.re
+++ b/src/Store/VimStoreConnector.re
@@ -284,7 +284,7 @@ let start =
Actions.OpenFileByPath(buf, Some(`Vertical), None)
| Vim.Types.Horizontal =>
Actions.OpenFileByPath(buf, Some(`Horizontal), None)
- | Vim.Types.TabPage => Actions.OpenFileByPath(buf, None, None)
+ | Vim.Types.TabPage => Actions.OpenFileInNewLayout(buf)
};
dispatch(command);
});
@@ -539,55 +539,29 @@ let start =
}
);
- let openFileByPathEffect = (state: State.t, filePath, location) =>
- Isolinear.Effect.create(~name="vim.openFileByPath", () => {
+ let openBufferEffect = (~onComplete, filePath) =>
+ Isolinear.Effect.create(~name="vim.openBuffer", () => {
let buffer = Vim.Buffer.openFile(filePath);
+ let bufferId = Vim.Buffer.getId(buffer);
- let () =
- location
- |> Option.iter((loc: Location.t) => {
- let cursor = (loc :> Vim.Cursor.t);
- let () = updateActiveEditorCursors([cursor]);
+ dispatch(onComplete(bufferId));
+ });
- let topLine: int = max(Index.toZeroBased(loc.line) - 10, 0);
+ let gotoLocationEffect = (editorId, location: Location.t) =>
+ Isolinear.Effect.create(~name="vim.gotoLocation", () => {
+ let cursor = (location :> Vim.Cursor.t);
+ updateActiveEditorCursors([cursor]);
- let editorId =
- Feature_Layout.activeEditor(state.layout) |> Editor.getId;
- dispatch(
- Actions.Editor({editorId, msg: ScrollToLine(topLine)}),
- );
- });
-
- let bufferId = Vim.Buffer.getId(buffer);
+ let topLine: int = max(Index.toZeroBased(location.line) - 10, 0);
- let maybeRenderer =
- switch (Core.BufferPath.parse(filePath)) {
- | Terminal({bufferId, _}) =>
- Some(
- BufferRenderer.Terminal({
- title: "Terminal",
- id: bufferId,
- insertMode: true,
- }),
- )
- | Version => Some(BufferRenderer.Version)
- | UpdateChangelog =>
- Some(
- BufferRenderer.UpdateChangelog({
- since: Persistence.Global.version(),
- }),
- )
- | Welcome => Some(BufferRenderer.Welcome)
- | Changelog => Some(BufferRenderer.FullChangelog)
- | FilePath(_) => None
- };
+ dispatch(Actions.Editor({editorId, msg: ScrollToLine(topLine)}));
+ });
- maybeRenderer
- |> Option.iter(renderer => {
- dispatch(
- Actions.BufferRenderer(RendererAvailable(bufferId, renderer)),
- )
- });
+ let addBufferRendererEffect = (bufferId, renderer) =>
+ Isolinear.Effect.create(~name="vim.addBufferRenderer", () => {
+ dispatch(
+ Actions.BufferRenderer(RendererAvailable(bufferId, renderer)),
+ )
});
let openTutorEffect =
@@ -793,7 +767,7 @@ let start =
| Init => (state, initEffect)
- | OpenFileByPath(path, maybeDirection, location) =>
+ | OpenFileByPath(path, maybeDirection, maybeLocation) =>
/* If a split was requested, create that first! */
let state' =
switch (maybeDirection) {
@@ -803,7 +777,64 @@ let start =
layout: Feature_Layout.split(direction, state.layout),
}
};
- (state', openFileByPathEffect(state', path, location));
+ (
+ state',
+ openBufferEffect(
+ ~onComplete=bufferId => BufferOpened(path, maybeLocation, bufferId),
+ path,
+ ),
+ );
+ | BufferOpened(path, maybeLocation, bufferId) =>
+ let maybeRenderer =
+ switch (Core.BufferPath.parse(path)) {
+ | Terminal({bufferId, _}) =>
+ Some(
+ BufferRenderer.Terminal({
+ title: "Terminal",
+ id: bufferId,
+ insertMode: true,
+ }),
+ )
+ | Version => Some(BufferRenderer.Version)
+ | UpdateChangelog =>
+ Some(
+ BufferRenderer.UpdateChangelog({
+ since: Persistence.Global.version(),
+ }),
+ )
+ | Welcome => Some(BufferRenderer.Welcome)
+ | Changelog => Some(BufferRenderer.FullChangelog)
+ | FilePath(_) => None
+ };
+
+ let editorId =
+ Feature_Layout.activeEditor(state.layout) |> Editor.getId;
+
+ (
+ state,
+ Isolinear.Effect.batch([
+ maybeRenderer
+ |> Option.map(addBufferRendererEffect(bufferId))
+ |> Option.value(~default=Isolinear.Effect.none),
+ maybeLocation
+ |> Option.map(gotoLocationEffect(editorId))
+ |> Option.value(~default=Isolinear.Effect.none),
+ ]),
+ );
+
+ | OpenFileInNewLayout(path) =>
+ let state = {
+ ...state,
+ layout: Feature_Layout.addLayoutTab(state.layout),
+ };
+ (
+ state,
+ openBufferEffect(
+ ~onComplete=bufferId => BufferOpenedForLayout(bufferId),
+ path,
+ ),
+ );
+ | BufferOpenedForLayout(_bufferId) => (state, Isolinear.Effect.none)
| Terminal(Command(NormalMode)) =>
let maybeBufferId =
diff --git a/src/UI/EditorView.re b/src/UI/EditorView.re
index 8728619c22..11b9e50b14 100644
--- a/src/UI/EditorView.re
+++ b/src/UI/EditorView.re
@@ -275,6 +275,7 @@ let make =
isZenMode={state.zenMode}
showTabs
model={state.layout}
+ config={Feature_Configuration.resolver(state.config)}
dispatch={msg => dispatch(Actions.Layout(msg))}>
...(module ContentProvider)