unit Myc.Fmx.AstEditor; interface uses System.SysUtils, System.Classes, System.Types, System.UITypes, System.Math, System.Math.Vectors, System.Generics.Collections, FMX.Types, FMX.Controls, FMX.Graphics, Myc.Ast, Myc.Ast.Nodes, Myc.Ast.Identities, Myc.Ast.Types, Myc.Ast.Visitor, Myc.Ast.Scope, Myc.Ast.Environment, Myc.Ast.Refactoring.Remove, Myc.Fmx.AstEditor.Render, Myc.Fmx.AstEditor.Node, Myc.Fmx.AstEditor.Workspace, Myc.Fmx.AstEditor.Visualizer; type TAstEditor = class(TControl) private type TNodeMap = TDictionary; TUndoStack = TStack; TTypePropagator = class(TAstVisitor) private FMap: TNodeMap; procedure ApplyType(const Node: IAstNode); protected function Accept(const Node: IAstNode): TVoid; override; public constructor Create(AMap: TNodeMap); end; private FEnv: TAstEnvironment; FWorkspace: TWorkspace; FNodeMap: TNodeMap; FRootViewNode: TAstViewNode; FFocusedNode: TAstViewNode; FCurrentAst: IAstNode; FUndoStack: TUndoStack; FRedoStack: TUndoStack; FIsUndoing: Boolean; FOnPaintConnections: TOnPaintConnectionsEvent; FOnWorkspaceMouseDown: TMouseEvent; procedure CollectNodes(Parent: TVisualNode); procedure ClearVisuals; procedure ApplyErrors(const Log: ICompilerLog); procedure ApplyTypes(const TypedAst: IAstNode); procedure HandleNodeDragDrop(ASource, ATarget: TVisualNode; AIndex: Integer); procedure HandleKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState); procedure HandleWorkspacePaintConnections(ASender: TObject; ACanvas: TCanvas); procedure HandleWorkspaceMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure CommitSnapshot; procedure EnsureVisible(Node: TAstViewNode); function GetNextVerticalNeighbor(StartNode: TAstViewNode): TAstViewNode; function GetPrevVerticalNeighbor(StartNode: TAstViewNode): TAstViewNode; function GetNextLinearNode(StartNode: TAstViewNode): TAstViewNode; function GetPrevLinearNode(StartNode: TAstViewNode): TAstViewNode; procedure SetOnPaintConnections(const Value: TOnPaintConnectionsEvent); protected procedure Paint; override; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Init(const AEnv: TAstEnvironment); procedure ValidateAndShow; procedure SetRoot(const AstNode: IAstNode; AClearHistory: Boolean = True); procedure Undo; procedure Redo; function CanUndo: Boolean; function CanRedo: Boolean; procedure SetFocusedNode(Node: TAstViewNode); procedure Navigate(Key: Word; Shift: TShiftState); property CurrentAst: IAstNode read FCurrentAst; property FocusedNode: TAstViewNode read FFocusedNode; published property Align; property Anchors; property Position; property Width; property Height; property Visible; property Enabled; property Opacity; property OnPaintConnections: TOnPaintConnectionsEvent read FOnPaintConnections write SetOnPaintConnections; property OnMouseDown: TMouseEvent read FOnWorkspaceMouseDown write FOnWorkspaceMouseDown; end; implementation uses Myc.Data.Value; { TAstEditor } constructor TAstEditor.Create(AOwner: TComponent); begin inherited Create(AOwner); Width := 400; Height := 300; ClipChildren := True; FWorkspace := TWorkspace.Create(Self); FWorkspace.Parent := Self; FWorkspace.Align := TAlignLayout.Client; FWorkspace.Stored := False; FWorkspace.Locked := True; FWorkspace.OnNodeDragDrop := HandleNodeDragDrop; FWorkspace.OnKeyDown := HandleKeyDown; FWorkspace.OnPaintConnections := HandleWorkspacePaintConnections; FWorkspace.OnMouseDown := HandleWorkspaceMouseDown; FNodeMap := TNodeMap.Create; FUndoStack := TUndoStack.Create; FRedoStack := TUndoStack.Create; FIsUndoing := False; FCurrentAst := nil; end; destructor TAstEditor.Destroy; begin SetRoot(nil); FNodeMap.Free; FUndoStack.Free; FRedoStack.Free; inherited; end; procedure TAstEditor.Init(const AEnv: TAstEnvironment); begin FEnv := AEnv; end; procedure TAstEditor.Paint; begin inherited; if (csDesigning in ComponentState) then begin Canvas.Stroke.Kind := TBrushKind.Solid; Canvas.Stroke.Color := TAlphaColors.Gray; Canvas.Stroke.Dash := TStrokeDash.Dash; Canvas.DrawRect(LocalRect, 0, 0, AllCorners, 1.0); end; end; procedure TAstEditor.SetOnPaintConnections(const Value: TOnPaintConnectionsEvent); begin FOnPaintConnections := Value; end; procedure TAstEditor.HandleWorkspacePaintConnections(ASender: TObject; ACanvas: TCanvas); begin if Assigned(FOnPaintConnections) then FOnPaintConnections(Self, ACanvas); end; procedure TAstEditor.HandleWorkspaceMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single); begin if FWorkspace.CanFocus then FWorkspace.SetFocus; var WorldP := FWorkspace.ScreenToWorld(FWorkspace.LocalToScreen(TPointF.Create(X, Y))); var Hit := FWorkspace.RootNode.HitTest(WorldP); if (Button = TMouseButton.mbLeft) and (Hit is TAstViewNode) then begin SetFocusedNode(TAstViewNode(Hit)); end; if Assigned(FOnWorkspaceMouseDown) then FOnWorkspaceMouseDown(Self, Button, Shift, X, Y); end; procedure TAstEditor.CommitSnapshot; begin if Assigned(FCurrentAst) and (not FIsUndoing) then begin FUndoStack.Push(FCurrentAst); FRedoStack.Clear; end; end; procedure TAstEditor.Undo; var prevAst: IAstNode; begin if FUndoStack.Count = 0 then exit; FIsUndoing := True; try if Assigned(FCurrentAst) then FRedoStack.Push(FCurrentAst); prevAst := FUndoStack.Pop; SetRoot(prevAst); finally FIsUndoing := False; end; end; procedure TAstEditor.Redo; var nextAst: IAstNode; begin if FRedoStack.Count = 0 then exit; FIsUndoing := True; try if Assigned(FCurrentAst) then FUndoStack.Push(FCurrentAst); nextAst := FRedoStack.Pop; SetRoot(nextAst); finally FIsUndoing := False; end; end; function TAstEditor.CanUndo: Boolean; begin Result := FUndoStack.Count > 0; end; function TAstEditor.CanRedo: Boolean; begin Result := FRedoStack.Count > 0; end; procedure TAstEditor.HandleNodeDragDrop(ASource, ATarget: TVisualNode; AIndex: Integer); var SourceView, TargetView: TAstViewNode; ContainerOwner: TAstViewNode; Reorderable: IReorderable; SourceIdx: Integer; NewAst: IAstNode; begin if (ASource = nil) or (AIndex < 0) then exit; if not (ASource is TAstViewNode) then exit; if not (ATarget is TAstViewNode) then exit; SourceView := TAstViewNode(ASource); TargetView := TAstViewNode(ATarget); ContainerOwner := TargetView; if not Supports(ContainerOwner.Handler, IReorderable, Reorderable) then exit; if Reorderable.CanDrag(SourceView) and (SourceView.Parent = TargetView) then begin // Find current index of SourceView in Target SourceIdx := -1; for var i := 0 to TargetView.GetChildCount - 1 do begin if TargetView.GetChild(i) = SourceView then begin SourceIdx := i; Break; end; end; if SourceIdx >= 0 then begin CommitSnapshot; Reorderable.MoveChild(SourceIdx, AIndex); NewAst := FRootViewNode.CreateAst; SetRoot(NewAst, False); end; end; end; procedure TAstEditor.HandleKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState); var NewRoot: IAstNode; begin // Undo / Redo if (ssCtrl in Shift) then begin if (Key = vkZ) then begin if (ssShift in Shift) then begin if CanRedo then Redo; end else begin if CanUndo then Undo; end; Key := 0; Exit; end else if (Key = vkY) then begin if CanRedo then Redo; Key := 0; Exit; end; end; // Delete if (Key = vkDelete) then begin if Assigned(FFocusedNode) and Assigned(FFocusedNode.Node) and Assigned(FCurrentAst) then begin CommitSnapshot; if TAstNodeRemover.TryRemove(FCurrentAst, FFocusedNode.Node, NewRoot) then begin SetRoot(NewRoot, False); end; end; Key := 0; Exit; end; // Navigation if Key in [vkLeft, vkRight, vkUp, vkDown, vkTab, vkEscape] then begin Navigate(Key, Shift); Key := 0; end; end; procedure TAstEditor.SetRoot(const AstNode: IAstNode; AClearHistory: Boolean); var Visualizer: IAstVisualizer; begin FCurrentAst := AstNode; if AClearHistory and (not FIsUndoing) then begin FUndoStack.Clear; FRedoStack.Clear; end; FFocusedNode := nil; FWorkspace.RootNode := nil; if Assigned(FRootViewNode) then FRootViewNode := nil; if not Assigned(AstNode) then exit; Visualizer := TAstVisualizer.Create(FWorkspace, nil, 0); FRootViewNode := Visualizer.CallAccept(AstNode); if Assigned(FRootViewNode) then begin FWorkspace.RootNode := FRootViewNode; FRootViewNode.Position := TPointF.Create(50, 50); ValidateAndShow; end; end; procedure TAstEditor.CollectNodes(Parent: TVisualNode); var i: Integer; child: TVisualNode; viewNode: TAstViewNode; begin for i := 0 to Parent.GetChildCount - 1 do begin child := Parent.GetChild(i); if child is TAstViewNode then begin viewNode := TAstViewNode(child); if Assigned(viewNode.Node) and Assigned(viewNode.Node.Identity) then begin FNodeMap.AddOrSetValue(viewNode.Node.Identity, viewNode); end; end; if child.GetChildCount > 0 then CollectNodes(child); end; end; procedure TAstEditor.ClearVisuals; begin for var viewNode in FNodeMap.Values do begin viewNode.ErrorMessage := ''; viewNode.TypeInfoText := ''; end; end; procedure TAstEditor.ApplyErrors(const Log: ICompilerLog); var entry: TCompilerError; viewNode: TAstViewNode; begin for entry in Log.GetEntries do begin if (entry.Level = elError) and Assigned(entry.Node) and Assigned(entry.Node.Identity) then begin if FNodeMap.TryGetValue(entry.Node.Identity, viewNode) then begin if viewNode.ErrorMessage = '' then viewNode.ErrorMessage := entry.Message else viewNode.ErrorMessage := viewNode.ErrorMessage + sLineBreak + entry.Message; end; end; end; end; procedure TAstEditor.ApplyTypes(const TypedAst: IAstNode); var propagator: TTypePropagator; begin if not Assigned(TypedAst) then exit; propagator := TTypePropagator.Create(FNodeMap); try propagator.Accept(TypedAst); finally propagator.Free; end; end; procedure TAstEditor.ValidateAndShow; var logicalAst: IAstNode; begin if not Assigned(FRootViewNode) then exit; logicalAst := FRootViewNode.CreateAst; FNodeMap.Clear; CollectNodes(FWorkspace.RootNode); ClearVisuals; try var log := TCompilerLog.Create as ICompilerLog; var expanded := FEnv.ExpandMacros(logicalAst); var typedAst := FEnv.Bind(expanded, [], log); var specAst := FEnv.Specialize(typedAst); ApplyTypes(specAst); if specAst.Kind = akLambdaExpression then var compiled := FEnv.Link(specAst.AsLambdaExpression, log); ApplyErrors(log); FWorkspace.RootNode.RecalcLayout; FWorkspace.InvalidateConnections; except on E: ECompilationFailed do begin var tempLog := TCompilerLog.Create as ICompilerLog; for var err in E.Errors do tempLog.Add(err.Level, err.Message, err.Node); ApplyErrors(tempLog); end; end; FWorkspace.RequestLayout; end; procedure TAstEditor.SetFocusedNode(Node: TAstViewNode); begin if FFocusedNode = Node then Exit; if Assigned(FFocusedNode) then FFocusedNode.IsFocused := False; FFocusedNode := Node; if Assigned(FFocusedNode) then begin FFocusedNode.IsFocused := True; EnsureVisible(FFocusedNode); end; end; procedure TAstEditor.EnsureVisible(Node: TAstViewNode); const cMargin = 40; var NodeRect: TRectF; begin if (Node = nil) or (FWorkspace = nil) then Exit; NodeRect := Node.AbsoluteRect; var ScreenCenter := TRectF.Create(0, 0, FWorkspace.Width, FWorkspace.Height).CenterPoint; var NodeCenterWorld := NodeRect.CenterPoint; FWorkspace.ContentOffset := ScreenCenter - (NodeCenterWorld * FWorkspace.Zoom); end; function TAstEditor.GetNextLinearNode(StartNode: TAstViewNode): TAstViewNode; function FindFirst(Node: TVisualNode): TAstViewNode; begin if Node is TAstViewNode then Exit(TAstViewNode(Node)); for var i := 0 to Node.GetChildCount - 1 do begin Result := FindFirst(Node.GetChild(i)); if Result <> nil then Exit; end; Result := nil; end; function FindNext(Node: TVisualNode): TAstViewNode; var Parent: TVisualNode; Idx: Integer; begin Result := nil; if Node.GetChildCount > 0 then begin Result := FindFirst(Node.GetChild(0)); if Result <> nil then Exit; end; Parent := Node.Parent; while Parent <> nil do begin Idx := -1; for var k := 0 to Parent.GetChildCount - 1 do if Parent.GetChild(k) = Node then begin Idx := k; break; end; if (Idx >= 0) and (Idx < Parent.GetChildCount - 1) then begin for var k := Idx + 1 to Parent.GetChildCount - 1 do begin Result := FindFirst(Parent.GetChild(k)); if Result <> nil then Exit; end; end; Node := Parent; Parent := Node.Parent; end; end; begin for var i := 0 to StartNode.GetChildCount - 1 do begin Result := FindFirst(StartNode.GetChild(i)); if Result <> nil then Exit; end; Result := FindNext(StartNode); end; function TAstEditor.GetPrevLinearNode(StartNode: TAstViewNode): TAstViewNode; begin Result := nil; end; function TAstEditor.GetNextVerticalNeighbor(StartNode: TAstViewNode): TAstViewNode; begin Result := GetNextLinearNode(StartNode); end; function TAstEditor.GetPrevVerticalNeighbor(StartNode: TAstViewNode): TAstViewNode; begin Result := GetPrevLinearNode(StartNode); end; procedure TAstEditor.Navigate(Key: Word; Shift: TShiftState); var Current: TAstViewNode; Target: TAstViewNode; begin if FFocusedNode = nil then begin SetFocusedNode(FRootViewNode); Exit; end; Current := FFocusedNode; Target := nil; if Key = vkTab then begin if ssShift in Shift then Target := GetPrevLinearNode(Current) else Target := GetNextLinearNode(Current); if Target <> nil then SetFocusedNode(Target); Exit; end; case Key of vkLeft: Target := GetPrevLinearNode(Current); vkRight: Target := GetNextLinearNode(Current); vkUp: Target := GetPrevVerticalNeighbor(Current); vkDown: Target := GetNextVerticalNeighbor(Current); vkEscape: if Current.Parent is TAstViewNode then Target := TAstViewNode(Current.Parent); end; if Target <> nil then SetFocusedNode(Target); end; { TAstEditor.TTypePropagator } constructor TAstEditor.TTypePropagator.Create(AMap: TNodeMap); begin inherited Create; FMap := AMap; end; procedure TAstEditor.TTypePropagator.ApplyType(const Node: IAstNode); var viewNode: TAstViewNode; begin if Assigned(Node) and Assigned(Node.Identity) then begin if FMap.TryGetValue(Node.Identity, viewNode) then begin if Node.IsTyped then begin var st := Node.AsTypedNode.StaticType; if Assigned(st) then viewNode.TypeInfoText := st.ToString; end; end; end; end; function TAstEditor.TTypePropagator.Accept(const Node: IAstNode): TVoid; begin if Node = nil then exit; ApplyType(Node); Node.Accept(Self); end; end.