Resolve BlockNum in WSH commands (#301)

This commit is contained in:
Evan Simkowitz 2024-08-30 20:20:25 -07:00 committed by GitHub
parent be1ce1f71e
commit aab487541b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 98 additions and 30 deletions

View File

@ -134,7 +134,7 @@ func validateEasyORef(oref string) error {
}
_, err := uuid.Parse(oref)
if err != nil {
return fmt.Errorf("invalid OID (must be UUID): %v", err)
return fmt.Errorf("invalid object reference (must be UUID, or a positive nonzero integer): %v", err)
}
return nil
}

View File

@ -1,7 +1,7 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { atomWithThrottle, boundNumber } from "@/util/util";
import { atomWithThrottle, boundNumber, lazy } from "@/util/util";
import { Atom, atom, Getter, PrimitiveAtom, Setter } from "jotai";
import { splitAtom } from "jotai/utils";
import { createRef, CSSProperties } from "react";
@ -110,7 +110,7 @@ export class LayoutModel {
/**
* An ordered list of node ids starting from the top left corner to the bottom right corner.
*/
leafOrder: PrimitiveAtom<string[]>;
leafOrder: PrimitiveAtom<LeafOrderEntry[]>;
/**
* Atom representing the number of leaf nodes in a layout.
*/
@ -223,6 +223,7 @@ export class LayoutModel {
this.halfResizeHandleSizePx = this.gapSizePx > 5 ? this.gapSizePx : DefaultGapSizePx;
this.resizeHandleSizePx = 2 * this.halfResizeHandleSizePx;
this.animationTimeS = animationTimeS ?? DefaultAnimationTimeS;
this.lastTreeStateGeneration = -1;
this.leafs = atom([]);
this.leafOrder = atom([]);
@ -283,7 +284,7 @@ export class LayoutModel {
return this.getPlaceholderTransform(pendingAction);
});
this.updateTreeState(true);
this.onTreeStateAtomUpdated(true);
}
/**
@ -357,24 +358,23 @@ export class LayoutModel {
default:
console.error("Invalid reducer action", this.treeState, action);
}
if (this.lastTreeStateGeneration !== this.treeState.generation) {
this.lastTreeStateGeneration = this.treeState.generation;
if (this.lastTreeStateGeneration < this.treeState.generation) {
if (this.magnifiedNodeId !== this.treeState.magnifiedNodeId) {
this.lastMagnifiedNodeId = this.magnifiedNodeId;
this.magnifiedNodeId = this.treeState.magnifiedNodeId;
}
this.updateTree();
this.setter(this.treeStateAtom, this.treeState);
this.setTreeStateAtom(true);
}
}
/**
* Callback that is invoked when the tree state has been updated on the backend. This ensures the model is updated if the atom is not fully loaded when the model is first instantiated.
* @param force Whether to force the tree state to update, regardless of whether the state is already up to date.
* Callback that is invoked when the upstream tree state has been updated. This ensures the model is updated if the atom is not fully loaded when the model is first instantiated.
* @param force Whether to force the local tree state to update, regardless of whether the state is already up to date.
*/
async updateTreeState(force = false) {
async onTreeStateAtomUpdated(force = false) {
const treeState = this.getter(this.treeStateAtom);
// Only update the local tree state if it is different from the one in the backend. This function is called even when the update was initiated by the LayoutModel, so we need to filter out false positives or we'll enter an infinite loop.
// Only update the local tree state if it is different from the one in the upstream atom. This function is called even when the update was initiated by the LayoutModel, so we need to filter out false positives or we'll enter an infinite loop.
if (
force ||
!this.treeState?.rootNode ||
@ -439,15 +439,34 @@ export class LayoutModel {
}
} else {
this.updateTree();
this.setTreeStateAtom();
}
}
}
/**
* Set the upstream tree state atom to the value of the local tree state.
* @param bumpGeneration Whether to bump the generation of the tree state before setting the atom.
*/
setTreeStateAtom(bumpGeneration = true) {
if (bumpGeneration) {
this.treeState.generation++;
}
this.lastTreeStateGeneration = this.treeState.generation;
this.setter(this.treeStateAtom, this.treeState);
}
/**
* This is a hack to ensure that when the updateTree first successfully runs, we set the upstream atom state to persist the initial leaf order.
* @see updateTree should be the only caller of this method.
*/
setTreeStateAtomOnce = lazy(() => this.setTreeStateAtom());
/**
* Recursively walks the tree to find leaf nodes, update the resize handles, and compute additional properties for each node.
* @param balanceTree Whether the tree should also be balanced as it is walked. This should be done if the tree state has just been updated. Defaults to true.
*/
updateTree = (balanceTree: boolean = true) => {
updateTree(balanceTree: boolean = true) {
if (this.displayContainerRef.current) {
const newLeafs: LayoutNode[] = [];
const newAdditionalProps = {};
@ -471,8 +490,9 @@ export class LayoutModel {
this.setter(this.leafOrder, this.treeState.leafOrder);
this.validateFocusedNode(this.treeState.leafOrder);
this.cleanupNodeModels();
this.setTreeStateAtomOnce();
}
};
}
/**
* Per-node callback that is invoked recursively to find leaf nodes, update the resize handles, and compute additional properties associated with the given node.
@ -595,12 +615,13 @@ export class LayoutModel {
* Checks whether the focused node id has changed and, if so, whether to update the focused node stack. If the focused node was deleted, will pop the latest value from the stack.
* @param leafOrder The new leaf order array to use when searching for stale nodes in the stack.
*/
private validateFocusedNode(leafOrder: string[]) {
private validateFocusedNode(leafOrder: LeafOrderEntry[]) {
if (this.treeState.focusedNodeId !== this.focusedNodeId) {
// Remove duplicates and stale entries from focus stack.
const newFocusedNodeIdStack: string[] = [];
for (const id of this.focusedNodeIdStack) {
if (leafOrder.includes(id) && !newFocusedNodeIdStack.includes(id)) newFocusedNodeIdStack.push(id);
if (leafOrder.find((leafEntry) => leafEntry.nodeid === id) && !newFocusedNodeIdStack.includes(id))
newFocusedNodeIdStack.push(id);
}
this.focusedNodeIdStack = newFocusedNodeIdStack;
@ -610,7 +631,7 @@ export class LayoutModel {
this.treeState.focusedNodeId = this.focusedNodeIdStack.shift();
} else {
// If no nodes are in the stack, use the top left node in the layout.
this.treeState.focusedNodeId = leafOrder[0];
this.treeState.focusedNodeId = leafOrder[0].nodeid;
}
}
this.focusedNodeIdStack.unshift(this.treeState.focusedNodeId);
@ -734,7 +755,7 @@ export class LayoutModel {
}),
nodeId: nodeid,
blockId,
blockNum: atom((get) => get(this.leafOrder).indexOf(nodeid) + 1),
blockNum: atom((get) => get(this.leafOrder).findIndex((leafEntry) => leafEntry.nodeid === nodeid) + 1),
isFocused: atom((get) => {
const treeState = get(this.treeStateAtom);
const isFocused = treeState.focusedNodeId === nodeid;
@ -759,7 +780,9 @@ export class LayoutModel {
private cleanupNodeModels() {
const leafOrder = this.getter(this.leafOrder);
const orphanedNodeModels = [...this.nodeModels.keys()].filter((id) => !leafOrder.includes(id));
const orphanedNodeModels = [...this.nodeModels.keys()].filter(
(id) => !leafOrder.find((leafEntry) => leafEntry.nodeid == id)
);
for (const id of orphanedNodeModels) {
this.nodeModels.delete(id);
}
@ -774,7 +797,7 @@ export class LayoutModel {
// If no node is focused, set focus to the first leaf.
if (!curNodeId) {
this.focusNode(this.getter(this.leafOrder)[0]);
this.focusNode(this.getter(this.leafOrder)[0].nodeid);
return;
}
@ -841,8 +864,8 @@ export class LayoutModel {
if (newLeafIdx < 0 || newLeafIdx >= leafOrder.length) {
return;
}
const leafId = leafOrder[newLeafIdx];
this.focusNode(leafId);
const leaf = leafOrder[newLeafIdx];
this.focusNode(leaf.nodeid);
}
/**
@ -1087,12 +1110,15 @@ export class LayoutModel {
}
}
function getLeafOrder(leafs: LayoutNode[], additionalProps: Record<string, LayoutNodeAdditionalProps>): string[] {
function getLeafOrder(
leafs: LayoutNode[],
additionalProps: Record<string, LayoutNodeAdditionalProps>
): LeafOrderEntry[] {
return leafs
.map((node) => node.id)
.map((node) => ({ nodeid: node.id, blockid: node.data.blockId }) as LeafOrderEntry)
.sort((a, b) => {
const treeKeyA = additionalProps[a]?.treeKey;
const treeKeyB = additionalProps[b]?.treeKey;
const treeKeyA = additionalProps[a.nodeid]?.treeKey;
const treeKeyB = additionalProps[b.nodeid]?.treeKey;
if (!treeKeyA || !treeKeyB) return;
return treeKeyA.localeCompare(treeKeyB);
});

View File

@ -24,7 +24,7 @@ export function getLayoutModelForTab(tabAtom: Atom<Tab>): LayoutModel {
}
const layoutTreeStateAtom = withLayoutTreeStateAtomFromTab(tabAtom);
const layoutModel = new LayoutModel(layoutTreeStateAtom, globalStore.get, globalStore.set);
globalStore.sub(layoutTreeStateAtom, () => fireAndForget(() => layoutModel.updateTreeState()));
globalStore.sub(layoutTreeStateAtom, () => fireAndForget(() => layoutModel.onTreeStateAtomUpdated()));
layoutModelMap.set(tabId, layoutModel);
return layoutModel;
}

View File

@ -256,7 +256,7 @@ export type LayoutTreeState = {
/**
* A computed ordered list of leafs in the layout. This value is driven by the LayoutModel and should not be read when updated from the backend.
*/
leafOrder?: string[];
leafOrder?: LeafOrderEntry[];
pendingBackendActions: LayoutActionData[];
generation: number;
};

View File

@ -225,10 +225,16 @@ declare global {
rootnode?: any;
magnifiednodeid?: string;
focusednodeid?: string;
leaforder?: string[];
leaforder?: LeafOrderEntry[];
pendingbackendactions?: LayoutActionData[];
};
// waveobj.LeafOrderEntry
type LeafOrderEntry = {
nodeid: string;
blockid: string;
};
// waveobj.MetaTSType
type MetaType = {
view?: string;

View File

@ -182,13 +182,18 @@ type LayoutActionData struct {
Magnified bool `json:"magnified"`
}
type LeafOrderEntry struct {
NodeId string `json:"nodeid"`
BlockId string `json:"blockid"`
}
type LayoutState struct {
OID string `json:"oid"`
Version int `json:"version"`
RootNode any `json:"rootnode,omitempty"`
MagnifiedNodeId string `json:"magnifiednodeid,omitempty"`
FocusedNodeId string `json:"focusednodeid,omitempty"`
LeafOrder *[]string `json:"leaforder,omitempty"`
LeafOrder *[]LeafOrderEntry `json:"leaforder,omitempty"`
PendingBackendActions *[]LayoutActionData `json:"pendingbackendactions,omitempty"`
Meta MetaMapType `json:"meta,omitempty"`
}

View File

@ -12,6 +12,8 @@ import (
"fmt"
"io/fs"
"log"
"regexp"
"strconv"
"strings"
"time"
@ -32,6 +34,8 @@ import (
const SimpleId_This = "this"
var SimpleId_BlockNum_Regex = regexp.MustCompile(`^\d+$`)
type WshServer struct{}
func (*WshServer) WshServerImpl() {}
@ -182,7 +186,34 @@ func resolveSimpleId(ctx context.Context, data wshrpc.CommandResolveIdsData, sim
}
return &waveobj.ORef{OType: waveobj.OType_Block, OID: data.BlockId}, nil
}
if strings.Contains(simpleId, ":") {
blockNum, err := strconv.Atoi(simpleId)
if err == nil {
tabId, err := wstore.DBFindTabForBlockId(ctx, data.BlockId)
if err != nil {
return nil, fmt.Errorf("error finding tab for blockid %s: %w", data.BlockId, err)
}
tab, err := wstore.DBGet[*waveobj.Tab](ctx, tabId)
if err != nil {
return nil, fmt.Errorf("error retrieving tab %s: %w", tabId, err)
}
layout, err := wstore.DBGet[*waveobj.LayoutState](ctx, tab.LayoutState)
if err != nil {
return nil, fmt.Errorf("error retrieving layout state %s: %w", tab.LayoutState, err)
}
if layout.LeafOrder == nil {
return nil, fmt.Errorf("could not resolve block num %v, leaf order is empty", blockNum)
}
leafIndex := blockNum - 1 // block nums are 1-indexed, we need the 0-indexed version
if len(*layout.LeafOrder) <= leafIndex {
return nil, fmt.Errorf("could not find a node in the layout matching blockNum %v", blockNum)
}
leafEntry := (*layout.LeafOrder)[leafIndex]
return &waveobj.ORef{OType: waveobj.OType_Block, OID: leafEntry.BlockId}, nil
} else if strings.Contains(simpleId, ":") {
rtn, err := waveobj.ParseORef(simpleId)
if err != nil {
return nil, fmt.Errorf("error parsing simple id: %w", err)