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)