diff --git a/src/App.tsx b/src/App.tsx index f58b101..9ec1717 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,8 @@ +import React from 'react'; import logo from './logo.svg'; import styles from './App.module.scss'; -import React from 'react'; import { CallstackItem, CallstackWindow } from './components/Callstack'; -import Splitter, { SplitDirection } from '@devbookhq/splitter'; - import { PrimaryButton, ThemeProvider, @@ -20,308 +18,238 @@ import { IRefObject, } from '@fluentui/react'; -import lightTheme from './themes/light'; -import darkTheme from './themes/dark'; -import { Stack, StackFrame, StackAllocation } from './components/Stack/Watch'; -import StackWindow from './components/Stack/Window'; -import LogWindow, { ILogEntry } from './components/Log/Window'; +import lightTheme from './themes/light'; +import darkTheme from './themes/dark'; +import { Stack, StackFrame, StackAllocation } from './components/Stack/Watch'; +import StackWindow from './components/Stack/Window'; +import LogWindow, { ILogEntry } from './components/Log/Window'; import LogData, { logMockupData } from './components/Log/Context'; +import StackData from './components/Stack/Context'; +import { SplitPane } from './components'; +import { ConnectionState } from './Connection'; +import { SettingsPanel } from './components/SettingsPanel/SettingsPanel'; -enum ConnectionState { - Disconnected, - Connecting, - Connected, -} -function toString(state: ConnectionState) { - switch (state) { - case ConnectionState.Disconnected: - return 'Disconnected'; - case ConnectionState.Connecting: - return 'Connecting'; - case ConnectionState.Connected: - return 'Connected'; - } +export interface SplitPaneProps { + children: React.ReactNode; } -type AppState = { - connected: ConnectionState; - useDarkTheme: boolean; - highlightedAddresses?: [number, number]; - callStack: CallstackItem[]; - stack: any[]; +export function RootSplit({children}: SplitPaneProps) { + return ( + + {children} + + ); } -const stackWithVerticalGap : IStackTokens = { - childrenGap: 10, - padding: "10px 0", -}; - - -const defaultAddress = 'ws://localhost:9002'; - -class MainControlsProps { - connected: ConnectionState = ConnectionState.Disconnected; - addressRef?: IRefObject; - onReconnect: () => void = () => {}; +export function MainVerticalSplit({children}: SplitPaneProps) { + return ( + + {children} + + ); } -function MainControls(props: MainControlsProps) { - return ( - -

Connection status: {toString(props.connected)}

- - - - -
- ); +export function MainTopHorizontalSplit({children}: SplitPaneProps) { + return ( + + {children} + + ); } -class ThemeSettingsProps { - onToggledDarkTheme: (isDarkTheme: boolean) => void = () => {}; - defaultTheme: 'dark' | 'light' = 'dark'; -}; -function ThemeSettings(props: ThemeSettingsProps) { - const [useDarkTheme, setDarkTheme] = React.useState(props.defaultTheme === 'dark'); +export default function App() { - return ( - - { - props.onToggledDarkTheme?.(!useDarkTheme); - setDarkTheme( !useDarkTheme ); - } } - /> - - ); -} -export default class App extends React.Component { + let logEntries: ILogEntry[] = []; - ws: WebSocket | null; - serverAddressRef: React.RefObject; - - logList: React.RefObject; - logEntries: ILogEntry[] = []; + const [ws, setWebSocket] = React.useState(null); + let serverAddressRef = React.createRef(); + let logList = React.createRef(); - constructor(props: any) { - super(props); - - this.ws = null; - this.serverAddressRef = React.createRef(); - this.logList = React.createRef(); + const [stack, setStack] = React.useState([]); + const [callStack, setCallStack] = React.useState([]); + const [autoReconnect, setAutoReconnect] = React.useState(true); + const [memory, setMemory] = React.useState(new Int8Array([ + 53, 0, 0, 0, + 12, 237, 0, 255, + 0, 11, 40, 0, + 19, 0, 8, 0, + ])); + const [highlightedAddresses, setHighlightedAddresses] = React.useState<[number, number] | null>(null); - this.reconnect = this.reconnect.bind(this); + const [connected, setConnected] = React.useState(ConnectionState.Disconnected); + const [useDarkTheme, setUseDarkTheme] = React.useState(true); - this.state = { - stack: [], - callStack: [], - connected: ConnectionState.Disconnected, - useDarkTheme: true, + React.useEffect(() => { + if (connected === ConnectionState.Connected) { + setStack([]); } - } + }, [connected]); + + const pushToLog = React.useCallback((entry: ILogEntry) => { + logEntries.push(entry); + logList.current?.forceUpdate(); + }, [logEntries, logList]); + + const handleStackRequest = (json: any) => { + if (json.action === 'pushFrame') { + const data = { + kind: 'frame', + ...(json.data as StackFrame) + } + setStack(s => [...s, data]); + } + else if (json.action === 'popFrame') { - setHoveredAddress(address?: [number, number]) { - this.setState({ - highlightedAddresses: address - }); + // FIXME: seems like this doesnt work + const isStackFrame = (item: any) => item.action === 'pushFrame'; + setStack(s => { + const lastStackFrameIndex = s.slice().reverse().findIndex(isStackFrame); + return s.slice(0, lastStackFrameIndex) + }); + } + else if (json.action === 'allocate') { + const data = { + kind: 'allocation', + ...(json.data as StackAllocation) + } + setStack(s => [...s, data]); + } } - pushToLog(entry: ILogEntry) { - this.logEntries.push(entry); - this.logList.current?.forceUpdate(); - } + const reconnect = React.useCallback(() => { - handleStackRequest(json: any) { - if (json.action === 'pushFrame') { - this.setState(prev => { - const data = { - kind: 'frame', - ...(json.data as StackFrame) - } - return { ...prev, stack: [ ...prev.stack, data ]} - }) - } - else if (json.action === 'popFrame') { - this.setState(prev => { - // FIXME: seems like this doesnt work - const isStackFrame = (item: any) => item.action === 'pushFrame' - const lastStackFrameIndex = prev.stack.slice().reverse().findIndex(isStackFrame) - - return { ...prev, stack: prev.stack.slice(0, lastStackFrameIndex)} - }) - } - else if (json.action === 'allocate') { - this.setState(prev => { - const data = { - kind: 'allocation', - ...(json.data as StackAllocation) - } - return { ...prev, stack: [ ...prev.stack, data ]} - }) - } - } - - reconnect() { - - if (this.ws && this.ws.readyState === WebSocket.OPEN) { - this.ws.close(); + if (ws) { + ws.close(); + setWebSocket(null); } - this.setState({ - connected: ConnectionState.Connecting - }); + setConnected(ConnectionState.Connecting); const tryConnect = () => { - if (!this.serverAddressRef.current) + if (!serverAddressRef.current) return false; - - const addr = this.serverAddressRef.current.value || ""; + + const addr = serverAddressRef.current.value || ""; if (addr === "") return false; - - - this.ws = new WebSocket(addr); - - this.ws.onopen = () => { - this.setState({ - connected: ConnectionState.Connected - }); - } - this.ws.onclose = () => { - this.setState({ - connected: ConnectionState.Disconnected - }); - } - this.ws.onmessage = (event: MessageEvent) => { + let ws = new WebSocket(addr); + + ws.onopen = () => { setConnected(ConnectionState.Connected); } + ws.onclose = () => { setConnected(ConnectionState.Disconnected); } + + ws.onmessage = (event: MessageEvent) => { const json = JSON.parse(event.data); if (json.type === 'callstack') { if (json.action === 'push') { - this.setState(prev => { - return { ...prev, callStack: [ ...prev.callStack, json.data as CallstackItem ] } - }) + setCallStack([...callStack, json.data as CallstackItem]); } else if (json.action === 'pop') { - this.setState(prev => { - return { ...prev, callStack: prev.callStack.slice(0, -1) } - }) + setCallStack(callStack.slice(0, -1)); } } - else if (json.type === 'stack') { - this.handleStackRequest(json); - } + else if (json.type === 'stack') { + handleStackRequest(json); + } else if (json.type === 'log') { - this.pushToLog(json.data as ILogEntry); - + pushToLog(json.data as ILogEntry); } } + setWebSocket(ws); + return true; }; if (!tryConnect()) { - this.setState({ - connected: ConnectionState.Disconnected - }); + setConnected(ConnectionState.Disconnected); } - } + }, [callStack, pushToLog, serverAddressRef, ws]); - render() { - return ( - -
- - {this.leftPanel()} - - -
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
- -
- - - - - - - - - - - - {/* */} -
-
-
-
- ); - } + React.useEffect(() => { + if (autoReconnect && connected === ConnectionState.Disconnected) { + reconnect(); + } + }, [reconnect, connected, autoReconnect, ws]); - private leftPanel() { - return ( - - - this.reconnect()} addressRef={this.serverAddressRef}/> - - - this.setState({useDarkTheme: isDarkTheme})} defaultTheme='dark'/> - - - ); - } + return ( + +
+ + setAutoReconnect(ar)} + /> + + +
+ logo +

+ Edit src/App.js and save to reload. +

+ + Learn React + +
+ + + +
+ + + + + + + + + + +
+
+
+
+ ); } diff --git a/src/Connection.ts b/src/Connection.ts new file mode 100644 index 0000000..18f3455 --- /dev/null +++ b/src/Connection.ts @@ -0,0 +1,16 @@ +export enum ConnectionState { + Disconnected, + Connecting, + Connected, +} + +export function toString(state: ConnectionState) { + switch (state) { + case ConnectionState.Disconnected: + return 'Disconnected'; + case ConnectionState.Connecting: + return 'Connecting'; + case ConnectionState.Connected: + return 'Connected'; + } +} diff --git a/src/components/SettingsPanel/SettingsPanel.module.scss b/src/components/SettingsPanel/SettingsPanel.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/SettingsPanel/SettingsPanel.tsx b/src/components/SettingsPanel/SettingsPanel.tsx new file mode 100644 index 0000000..2bc61d8 --- /dev/null +++ b/src/components/SettingsPanel/SettingsPanel.tsx @@ -0,0 +1,103 @@ +import React from 'react'; + +import { + PrimaryButton, + Toggle, + TextField, + ITextField, + Stack as Stacker, + IStackTokens, + Pivot, + PivotItem, + IRefObject, +} from '@fluentui/react'; +import { ConnectionState, toString } from '../../Connection'; + +import styles from "./SettingsPanel.module.scss"; + +const stackWithVerticalGap: IStackTokens = { + childrenGap: 10, + padding: "10px 0", +}; + + +const defaultAddress = 'ws://localhost:9002'; + +class MainControlsProps { + connected: ConnectionState = ConnectionState.Disconnected; + addressRef?: IRefObject; + onReconnect: () => void = () => { }; + onAutoReconnectChanged: (autoReconnect: boolean) => void = () => { }; +} + +function MainControls(props: MainControlsProps) { + + const [autoReconnect, setAutoReconnect] = React.useState(true); + + return ( + +

Connection status: {toString(props.connected)}

+ { + props.onAutoReconnectChanged(!autoReconnect); + setAutoReconnect(!autoReconnect); + }} + /> + + + + +
+ ); +} + +class ThemeSettingsProps { + onToggledDarkTheme: (isDarkTheme: boolean) => void = () => { }; + defaultTheme: 'dark' | 'light' = 'dark'; +}; + +function ThemeSettings(props: ThemeSettingsProps) { + const [useDarkTheme, setDarkTheme] = React.useState(props.defaultTheme === 'dark'); + + return ( + + { + props.onToggledDarkTheme?.(!useDarkTheme); + setDarkTheme(!useDarkTheme); + }} + /> + + ); +} + +export function SettingsPanel({connected, reconnect, setUseDarkTheme, serverAddressRef, onAutoReconnectChanged}: any) { + return ( + + + + + + setUseDarkTheme(isDarkTheme)} defaultTheme='dark' /> + + + ); +} diff --git a/src/components/SplitPane/SplitPane.tsx b/src/components/SplitPane/SplitPane.tsx new file mode 100644 index 0000000..a17dcf8 --- /dev/null +++ b/src/components/SplitPane/SplitPane.tsx @@ -0,0 +1,68 @@ + +import React from 'react'; +import Splitter, { SplitDirection } from '@devbookhq/splitter'; + +// import cookies +import { + setCookie, + getCookie, +} from "../../helper/Cookies"; + + +export interface SizedSplitPaneProps { + children: React.ReactNode; + initialSize: [number, number]; + minimalSize: [number, number]; + direction: "x" | "y"; + cookieName?: string; +} + + +function translateDirection(direction: "x" | "y"): SplitDirection { + if (direction === "x") { + return SplitDirection.Horizontal; + } + return SplitDirection.Vertical; +} + +export default function SplitPane({ + children, + initialSize, + minimalSize, + direction, + cookieName + }: SizedSplitPaneProps) +{ + const [sizes, setSizes] = React.useState<[number, number]>(initialSize); + + React.useEffect(() => { + if (cookieName) { + const cookie = getCookie(cookieName); + if (cookie) { + setSizes(JSON.parse(cookie)); + } + } + }, [cookieName]); + + const handleResize = (sizes: [number, number]) => { + setSizes(sizes); + if (cookieName) { + setCookie(cookieName, JSON.stringify(sizes), 365); + } + }; + + return ( + handleResize(s as [number, number])} + {...(direction === "x" + ? + { minWidths: minimalSize } + : + { minHeights: minimalSize } + )} + > + {children} + + ); +} \ No newline at end of file diff --git a/src/components/SplitPane/index.ts b/src/components/SplitPane/index.ts new file mode 100644 index 0000000..7f2531b --- /dev/null +++ b/src/components/SplitPane/index.ts @@ -0,0 +1 @@ +export { default } from './SplitPane'; \ No newline at end of file diff --git a/src/components/Stack/Window.tsx b/src/components/Stack/Window.tsx index 20e8152..c77eca0 100644 --- a/src/components/Stack/Window.tsx +++ b/src/components/Stack/Window.tsx @@ -1,8 +1,7 @@ import React from 'react'; +import { SplitPane } from '../../components'; import styles from '../../App.module.scss'; -import Splitter, { SplitDirection } from '@devbookhq/splitter'; - import StackMemoryView from './MemoryView'; import StackWatchWindow, { Stack } from './Watch'; @@ -13,24 +12,23 @@ interface StackWindowProps { export default function StackWindow(props: StackWindowProps) { const [highlightedAddresses, setHighlightedAddresses] = React.useState<[number, number]>(); - const [sizes, setSizes] = React.useState<[number, number]>([70, 30]); return ( - {setSizes([ s[0], s[1] ])}} +
{}
setHighlightedAddresses([addr.address, addr.size])} onValueUnhovered={() => setHighlightedAddresses(undefined)} />
-
+ ); } diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..f500519 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1 @@ +export { default as SplitPane } from './SplitPane'; \ No newline at end of file diff --git a/src/helper/Cookies.ts b/src/helper/Cookies.ts new file mode 100644 index 0000000..3a0b0c4 --- /dev/null +++ b/src/helper/Cookies.ts @@ -0,0 +1,24 @@ +export function setCookie(name: string, value: any, days: number): void { + let expires = ""; + if (days) { + let date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = `; expires=${date.toUTCString()}`; + } + document.cookie = `${name}=${value || ""}${expires}; path=/`; +} + +export function getCookie(name: string): string | null { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +export function eraseCookie(name: string): void { + document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 6c95821..0ed1388 100644 --- a/src/index.css +++ b/src/index.css @@ -1,15 +1,19 @@ body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: rgb(44, 44, 44); - color: white; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: rgb(44, 44, 44); + color: white; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; } + +.app-window { + padding: 10px; +} \ No newline at end of file