From 476b3a264d2781ed7d1d833295c0fb5b7fe5348e Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 18 May 2023 12:07:06 +0800 Subject: [PATCH 01/12] Duplicate Playground as SicpWorkspace --- .../sicp/subcomponents/SicpWorkspace.tsx | 1057 +++++++++++++++++ .../subcomponents/SicpWorkspaceContainer.tsx | 4 +- 2 files changed, 1059 insertions(+), 2 deletions(-) create mode 100644 src/pages/sicp/subcomponents/SicpWorkspace.tsx diff --git a/src/pages/sicp/subcomponents/SicpWorkspace.tsx b/src/pages/sicp/subcomponents/SicpWorkspace.tsx new file mode 100644 index 0000000000..43371f0451 --- /dev/null +++ b/src/pages/sicp/subcomponents/SicpWorkspace.tsx @@ -0,0 +1,1057 @@ +import { Classes } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import { Octokit } from '@octokit/rest'; +import { Ace, Range } from 'ace-builds'; +import { FSModule } from 'browserfs/dist/node/core/FS'; +import classNames from 'classnames'; +import { Chapter, Variant } from 'js-slang/dist/types'; +import _, { isEqual } from 'lodash'; +import { decompressFromEncodedURIComponent } from 'lz-string'; +import * as React from 'react'; +import { HotKeys } from 'react-hotkeys'; +import { useDispatch, useStore } from 'react-redux'; +import { RouteComponentProps, useHistory, useLocation } from 'react-router'; +import { AnyAction, Dispatch } from 'redux'; +import { + beginDebuggerPause, + beginInterruptExecution, + debuggerReset, + debuggerResume +} from 'src/commons/application/actions/InterpreterActions'; +import { + loginGitHub, + logoutGitHub, + logoutGoogle +} from 'src/commons/application/actions/SessionActions'; +import { + setEditorSessionId, + setSharedbConnected +} from 'src/commons/collabEditing/CollabEditingActions'; +import { useResponsive, useTypedSelector } from 'src/commons/utils/Hooks'; +import { + showFullJSWarningOnUrlLoad, + showFulTSWarningOnUrlLoad, + showHTMLDisclaimer +} from 'src/commons/utils/WarningDialogHelper'; +import { + addEditorTab, + addHtmlConsoleError, + browseReplHistoryDown, + browseReplHistoryUp, + changeSideContentHeight, + changeStepLimit, + evalEditor, + navigateToDeclaration, + promptAutocomplete, + removeEditorTab, + removeEditorTabsForDirectory, + sendReplInputToOutput, + setEditorHighlightedLines, + setFolderMode, + toggleEditorAutorun, + toggleFolderMode, + toggleUpdateEnv, + updateActiveEditorTabIndex, + updateEnvSteps, + updateEnvStepsTotal, + updateReplValue +} from 'src/commons/workspace/WorkspaceActions'; +import { EditorTabState, WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; +import EnvVisualizer from 'src/features/envVisualizer/EnvVisualizer'; +import { + githubOpenFile, + githubSaveFile, + githubSaveFileAs +} from 'src/features/github/GitHubActions'; +import { + persistenceInitialise, + persistenceOpenPicker, + persistenceSaveFile, + persistenceSaveFileAs +} from 'src/features/persistence/PersistenceActions'; +import { + generateLzString, + playgroundConfigLanguage, + shortenURL, + updateShortURL +} from 'src/features/playground/PlaygroundActions'; +import { + dataVisualizerTab, + desktopOnlyTabIds, + makeEnvVisualizerTabFrom, + makeHtmlDisplayTabFrom, + makeIntroductionTabFrom, + makeRemoteExecutionTabFrom, + makeSubstVisualizerTabFrom, + mobileOnlyTabIds +} from 'src/pages/playground/PlaygroundTabs'; + +import { + getDefaultFilePath, + getLanguageConfig, + InterpreterOutput, + isSourceLanguage, + OverallState, + ResultOutput, + SALanguage +} from '../../../commons/application/ApplicationTypes'; +import { ExternalLibraryName } from '../../../commons/application/types/ExternalTypes'; +import { ControlBarAutorunButtons } from '../../../commons/controlBar/ControlBarAutorunButtons'; +import { ControlBarChapterSelect } from '../../../commons/controlBar/ControlBarChapterSelect'; +import { ControlBarClearButton } from '../../../commons/controlBar/ControlBarClearButton'; +import { ControlBarEvalButton } from '../../../commons/controlBar/ControlBarEvalButton'; +import { ControlBarExecutionTime } from '../../../commons/controlBar/ControlBarExecutionTime'; +import { ControlBarGoogleDriveButtons } from '../../../commons/controlBar/ControlBarGoogleDriveButtons'; +import { ControlBarSessionButtons } from '../../../commons/controlBar/ControlBarSessionButton'; +import { ControlBarShareButton } from '../../../commons/controlBar/ControlBarShareButton'; +import { ControlBarStepLimit } from '../../../commons/controlBar/ControlBarStepLimit'; +import { ControlBarToggleFolderModeButton } from '../../../commons/controlBar/ControlBarToggleFolderModeButton'; +import { ControlBarGitHubButtons } from '../../../commons/controlBar/github/ControlBarGitHubButtons'; +import { + convertEditorTabStateToProps, + NormalEditorContainerProps +} from '../../../commons/editor/EditorContainer'; +import { Position } from '../../../commons/editor/EditorTypes'; +import { overwriteFilesInWorkspace } from '../../../commons/fileSystem/utils'; +import FileSystemView from '../../../commons/fileSystemView/FileSystemView'; +import MobileWorkspace, { + MobileWorkspaceProps +} from '../../../commons/mobileWorkspace/MobileWorkspace'; +import { SideBarTab } from '../../../commons/sideBar/SideBar'; +import { SideContentTab, SideContentType } from '../../../commons/sideContent/SideContentTypes'; +import { Links } from '../../../commons/utils/Constants'; +import { generateSourceIntroduction } from '../../../commons/utils/IntroductionHelper'; +import { convertParamToBoolean, convertParamToInt } from '../../../commons/utils/ParamParseHelper'; +import { IParsedQuery, parseQuery } from '../../../commons/utils/QueryHelper'; +import Workspace, { WorkspaceProps } from '../../../commons/workspace/Workspace'; +import { initSession, log } from '../../../features/eventLogging'; +import { GitHubSaveInfo } from '../../../features/github/GitHubTypes'; +import { PersistenceFile } from '../../../features/persistence/PersistenceTypes'; +import { + CodeDelta, + Input, + SelectionRange +} from '../../../features/sourceRecorder/SourceRecorderTypes'; +import { WORKSPACE_BASE_PATHS } from '../../fileSystem/createInBrowserFileSystem'; + +export type PlaygroundProps = OwnProps & + DispatchProps & + StateProps & + RouteComponentProps<{}> & { + workspaceLocation?: WorkspaceLocation; + }; + +export type OwnProps = { + isSicpEditor?: boolean; + initialEditorValueHash?: string; + prependLength?: number; + handleCloseEditor?: () => void; +}; + +export type DispatchProps = { + handleChangeExecTime: (execTime: number) => void; + handleChapterSelect: (chapter: Chapter, variant: Variant) => void; + handleEditorValueChange: (editorTabIndex: number, newEditorValue: string) => void; + handleEditorUpdateBreakpoints: (editorTabIndex: number, newBreakpoints: string[]) => void; + handleReplEval: () => void; + handleReplOutputClear: () => void; + handleUsingEnv: (usingEnv: boolean) => void; + handleUsingSubst: (usingSubst: boolean) => void; +}; + +export type StateProps = { + editorTabs: EditorTabState[]; + programPrependValue: string; + programPostpendValue: string; + editorSessionId: string; + execTime: number; + isEditorAutorun: boolean; + isRunning: boolean; + isDebugging: boolean; + enableDebugging: boolean; + output: InterpreterOutput[]; + queryString?: string; + shortURL?: string; + replValue: string; + sideContentHeight?: number; + playgroundSourceChapter: Chapter; + playgroundSourceVariant: Variant; + courseSourceChapter?: number; + courseSourceVariant?: Variant; + stepLimit: number; + sharedbConnected: boolean; + usingEnv: boolean; + usingSubst: boolean; + persistenceUser: string | undefined; + persistenceFile: PersistenceFile | undefined; + githubOctokitObject: { octokit: Octokit | undefined }; + githubSaveInfo: GitHubSaveInfo; +}; + +const keyMap = { goGreen: 'h u l k' }; + +export async function handleHash( + hash: string, + props: PlaygroundProps, + workspaceLocation: WorkspaceLocation, + dispatch: Dispatch, + fileSystem: FSModule +) { + // Make the parsed query string object a Partial because we might access keys which are not set. + const qs: Partial = parseQuery(hash); + + const chapter = convertParamToInt(qs.chap) ?? undefined; + if (chapter === Chapter.FULL_JS) { + showFullJSWarningOnUrlLoad(); + } else if (chapter === Chapter.FULL_TS) { + showFulTSWarningOnUrlLoad(); + } else { + if (chapter === Chapter.HTML) { + const continueToHtml = await showHTMLDisclaimer(); + if (!continueToHtml) { + return; + } + } + + // For backward compatibility with old share links - 'prgrm' is no longer used. + const program = qs.prgrm === undefined ? '' : decompressFromEncodedURIComponent(qs.prgrm); + + // By default, create just the default file. + const defaultFilePath = getDefaultFilePath(workspaceLocation); + const files: Record = + qs.files === undefined + ? { + [defaultFilePath]: program + } + : parseQuery(decompressFromEncodedURIComponent(qs.files)); + await overwriteFilesInWorkspace(workspaceLocation, fileSystem, files); + + // BrowserFS does not provide a way of listening to changes in the file system, which makes + // updating the file system view troublesome. To force the file system view to re-render + // (and thus display the updated file system), we first disable Folder mode. + dispatch(setFolderMode(workspaceLocation, false)); + const isFolderModeEnabled = convertParamToBoolean(qs.isFolder) ?? false; + // If Folder mode should be enabled, enabling it after disabling it earlier will cause the + // newly-added files to be shown. Note that this has to take place after the files are + // already added to the file system. + dispatch(setFolderMode(workspaceLocation, isFolderModeEnabled)); + + // By default, open a single editor tab containing the default playground file. + const editorTabFilePaths = qs.tabs?.split(',').map(decompressFromEncodedURIComponent) ?? [ + defaultFilePath + ]; + // Remove all editor tabs before populating with the ones from the query string. + dispatch( + removeEditorTabsForDirectory(workspaceLocation, WORKSPACE_BASE_PATHS[workspaceLocation]) + ); + // Add editor tabs from the query string. + editorTabFilePaths.forEach(filePath => + // Fall back on the empty string if the file contents do not exist. + dispatch(addEditorTab(workspaceLocation, filePath, files[filePath] ?? '')) + ); + + // By default, use the first editor tab. + const activeEditorTabIndex = convertParamToInt(qs.tabIdx) ?? 0; + dispatch(updateActiveEditorTabIndex(workspaceLocation, activeEditorTabIndex)); + if (chapter) { + // TODO: To migrate the state logic away from playgroundSourceChapter + // and playgroundSourceVariant into the language config instead + const languageConfig = getLanguageConfig(chapter, qs.variant as Variant); + props.handleChapterSelect(chapter, languageConfig.variant); + // Hardcoded for Playground only for now, while we await workspace refactoring + // to decouple the SicpWorkspace from the Playground. + dispatch(playgroundConfigLanguage(languageConfig)); + } + + const execTime = Math.max(convertParamToInt(qs.exec || '1000') || 1000, 1000); + if (execTime) { + props.handleChangeExecTime(execTime); + } + } +} + +const SicpWorkspace: React.FC = ({ + workspaceLocation = 'playground', + ...props +}) => { + const { isSicpEditor } = props; + const { isMobileBreakpoint } = useResponsive(); + const propsRef = React.useRef(props); + propsRef.current = props; + + const dispatch = useDispatch(); + + const [deviceSecret, setDeviceSecret] = React.useState(); + const location = useLocation(); + const history = useHistory(); + const store = useStore(); + const searchParams = new URLSearchParams(location.search); + const shouldAddDevice = searchParams.get('add_device'); + + const { isFolderModeEnabled, activeEditorTabIndex } = useTypedSelector( + state => state.workspaces[workspaceLocation] + ); + const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); + + // Hide search query from URL to maintain an illusion of security. The device secret + // is still exposed via the 'Referer' header when requesting external content (e.g. Google API fonts) + if (shouldAddDevice && !deviceSecret) { + setDeviceSecret(shouldAddDevice); + history.replace(location.pathname); + } + + const [lastEdit, setLastEdit] = React.useState(new Date()); + const [isGreen, setIsGreen] = React.useState(false); + const [selectedTab, setSelectedTab] = React.useState( + shouldAddDevice ? SideContentType.remoteExecution : SideContentType.introduction + ); + const [hasBreakpoints, setHasBreakpoints] = React.useState(false); + const [sessionId, setSessionId] = React.useState(() => + initSession('playground', { + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + editorValue: propsRef.current.editorTabs[0]?.value ?? '', + chapter: propsRef.current.playgroundSourceChapter + }) + ); + + const remoteExecutionTab: SideContentTab = React.useMemo( + () => makeRemoteExecutionTabFrom(deviceSecret, setDeviceSecret), + [deviceSecret] + ); + + const usingRemoteExecution = + useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; + // this is still used by remote execution (EV3) + // specifically, for the editor Ctrl+B to work + const externalLibraryName = useTypedSelector( + state => state.workspaces.playground.externalLibrary + ); + + React.useEffect(() => { + // When the editor session Id changes, then treat it as a new session. + setSessionId( + initSession('playground', { + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + editorValue: propsRef.current.editorTabs[0]?.value ?? '', + chapter: propsRef.current.playgroundSourceChapter + }) + ); + }, [props.editorSessionId]); + + const hash = isSicpEditor ? props.initialEditorValueHash : props.location.hash; + + React.useEffect(() => { + if (!hash) { + // If not a accessing via shared link, use the Source chapter and variant in the current course + if (props.courseSourceChapter && props.courseSourceVariant) { + propsRef.current.handleChapterSelect(props.courseSourceChapter, props.courseSourceVariant); + // TODO: To migrate the state logic away from playgroundSourceChapter + // and playgroundSourceVariant into the language config instead + const languageConfig = getLanguageConfig( + props.courseSourceChapter, + props.courseSourceVariant + ); + // Hardcoded for Playground only for now, while we await workspace refactoring + // to decouple the SicpWorkspace from the Playground. + dispatch(playgroundConfigLanguage(languageConfig)); + // Disable Folder mode when forcing the Source chapter and variant to follow the current course's. + // This is because Folder mode only works in Source 2+. + dispatch(setFolderMode(workspaceLocation, false)); + } + return; + } + if (fileSystem !== null) { + handleHash(hash, propsRef.current, workspaceLocation, dispatch, fileSystem); + } + }, [ + dispatch, + fileSystem, + hash, + props.courseSourceChapter, + props.courseSourceVariant, + workspaceLocation + ]); + + /** + * Handles toggling of relevant SideContentTabs when mobile breakpoint it hit + */ + React.useEffect(() => { + if (isMobileBreakpoint && desktopOnlyTabIds.includes(selectedTab)) { + setSelectedTab(SideContentType.mobileEditor); + } else if (!isMobileBreakpoint && mobileOnlyTabIds.includes(selectedTab)) { + setSelectedTab(SideContentType.introduction); + } + }, [isMobileBreakpoint, selectedTab]); + + const handlers = React.useMemo( + () => ({ + goGreen: () => setIsGreen(!isGreen) + }), + [isGreen] + ); + + const onEditorValueChange = React.useCallback((editorTabIndex, newEditorValue) => { + setLastEdit(new Date()); + propsRef.current.handleEditorValueChange(editorTabIndex, newEditorValue); + }, []); + + const handleEnvVisualiserReset = React.useCallback(() => { + const { handleUsingEnv } = propsRef.current; + handleUsingEnv(false); + EnvVisualizer.clearEnv(); + dispatch(updateEnvSteps(-1, workspaceLocation)); + dispatch(updateEnvStepsTotal(0, workspaceLocation)); + dispatch(toggleUpdateEnv(true, workspaceLocation)); + dispatch(setEditorHighlightedLines(workspaceLocation, 0, [])); + }, [dispatch, workspaceLocation]); + + const onChangeTabs = React.useCallback( + ( + newTabId: SideContentType, + prevTabId: SideContentType, + event: React.MouseEvent + ) => { + if (newTabId === prevTabId) { + return; + } + + // Do nothing when clicking the mobile 'Run' tab while on the stepper tab. + if ( + prevTabId === SideContentType.substVisualizer && + newTabId === SideContentType.mobileEditorRun + ) { + return; + } + + const { handleUsingEnv, handleUsingSubst, handleReplOutputClear, playgroundSourceChapter } = + propsRef.current; + + if (newTabId !== SideContentType.envVisualizer) { + handleEnvVisualiserReset(); + } + + if ( + isSourceLanguage(playgroundSourceChapter) && + (newTabId === SideContentType.substVisualizer || newTabId === SideContentType.envVisualizer) + ) { + if (playgroundSourceChapter <= Chapter.SOURCE_2) { + handleUsingSubst(true); + } else { + handleUsingEnv(true); + } + } + + if (prevTabId === SideContentType.substVisualizer && !hasBreakpoints) { + handleReplOutputClear(); + handleUsingSubst(false); + } + + setSelectedTab(newTabId); + }, + [hasBreakpoints, handleEnvVisualiserReset] + ); + + const pushLog = React.useCallback( + (newInput: Input) => { + log(sessionId, newInput); + }, + [sessionId] + ); + + const memoizedHandlers = React.useMemo(() => { + return { + handleEditorEval: () => dispatch(evalEditor(workspaceLocation)), + handleInterruptEval: () => dispatch(beginInterruptExecution(workspaceLocation)), + handleToggleEditorAutorun: () => dispatch(toggleEditorAutorun(workspaceLocation)), + handleDebuggerPause: () => dispatch(beginDebuggerPause(workspaceLocation)), + handleDebuggerReset: () => dispatch(debuggerReset(workspaceLocation)), + handleDebuggerResume: () => dispatch(debuggerResume(workspaceLocation)) + }; + }, [dispatch, workspaceLocation]); + + const autorunButtons = React.useMemo(() => { + return ( + + ); + }, [ + activeEditorTabIndex, + props.isDebugging, + props.isEditorAutorun, + props.isRunning, + props.playgroundSourceChapter, + memoizedHandlers, + usingRemoteExecution + ]); + + const chapterSelectHandler = React.useCallback( + (sublanguage: SALanguage, e: any) => { + const { chapter, variant } = sublanguage; + const { handleUsingSubst, handleReplOutputClear, handleChapterSelect } = propsRef.current; + if ((chapter <= 2 && hasBreakpoints) || selectedTab === SideContentType.substVisualizer) { + handleUsingSubst(true); + } + if (chapter > 2) { + handleReplOutputClear(); + handleUsingSubst(false); + } + + const input: Input = { + time: Date.now(), + type: 'chapterSelect', + data: chapter + }; + + pushLog(input); + + handleChapterSelect(chapter, variant); + // Hardcoded for Playground only for now, while we await workspace refactoring + // to decouple the SicpWorkspace from the Playground. + dispatch(playgroundConfigLanguage(sublanguage)); + }, + [dispatch, hasBreakpoints, selectedTab, pushLog] + ); + + const chapterSelect = React.useMemo( + () => ( + + ), + [ + chapterSelectHandler, + isFolderModeEnabled, + props.playgroundSourceChapter, + props.playgroundSourceVariant, + usingRemoteExecution + ] + ); + + const clearButton = React.useMemo( + () => + selectedTab === SideContentType.substVisualizer ? null : ( + + ), + [props.handleReplOutputClear, selectedTab] + ); + + const evalButton = React.useMemo( + () => + selectedTab === SideContentType.substVisualizer ? null : ( + + ), + [props.handleReplEval, props.isRunning, selectedTab] + ); + + const { persistenceUser, persistenceFile } = props; + // Compute this here to avoid re-rendering the button every keystroke + const persistenceIsDirty = + persistenceFile && (!persistenceFile.lastSaved || persistenceFile.lastSaved < lastEdit); + const persistenceButtons = React.useMemo(() => { + return ( + dispatch(persistenceSaveFileAs())} + onClickOpen={() => dispatch(persistenceOpenPicker())} + onClickSave={ + persistenceFile ? () => dispatch(persistenceSaveFile(persistenceFile)) : undefined + } + onClickLogOut={() => dispatch(logoutGoogle())} + onPopoverOpening={() => dispatch(persistenceInitialise())} + /> + ); + }, [isFolderModeEnabled, persistenceFile, persistenceUser, persistenceIsDirty, dispatch]); + + const githubOctokitObject = useTypedSelector(store => store.session.githubOctokitObject); + const githubSaveInfo = props.githubSaveInfo; + const githubPersistenceIsDirty = + githubSaveInfo && (!githubSaveInfo.lastSaved || githubSaveInfo.lastSaved < lastEdit); + const githubButtons = React.useMemo(() => { + return ( + dispatch(githubOpenFile())} + onClickSaveAs={() => dispatch(githubSaveFileAs())} + onClickSave={() => dispatch(githubSaveFile())} + onClickLogIn={() => dispatch(loginGitHub())} + onClickLogOut={() => dispatch(logoutGitHub())} + /> + ); + }, [ + dispatch, + githubOctokitObject.octokit, + githubPersistenceIsDirty, + githubSaveInfo, + isFolderModeEnabled + ]); + + const executionTime = React.useMemo( + () => ( + + ), + [props.execTime, props.handleChangeExecTime] + ); + + const stepperStepLimit = React.useMemo( + () => ( + dispatch(changeStepLimit(limit, workspaceLocation))} + handleOnBlurAutoScale={limit => { + limit % 2 === 0 + ? dispatch(changeStepLimit(limit, workspaceLocation)) + : dispatch(changeStepLimit(limit + 1, workspaceLocation)); + }} + key="step_limit" + /> + ), + [dispatch, props.stepLimit, workspaceLocation] + ); + + const getEditorValue = React.useCallback( + // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. + () => store.getState().workspaces[workspaceLocation].editorTabs[0].value, + [store, workspaceLocation] + ); + + const sessionButtons = React.useMemo( + () => ( + dispatch(setEditorSessionId(workspaceLocation, id))} + sharedbConnected={props.sharedbConnected} + key="session" + /> + ), + [ + dispatch, + getEditorValue, + isFolderModeEnabled, + props.editorSessionId, + props.sharedbConnected, + workspaceLocation + ] + ); + + const shareButton = React.useMemo(() => { + const queryString = isSicpEditor + ? Links.playground + '#' + props.initialEditorValueHash + : props.queryString; + return ( + dispatch(generateLzString())} + handleShortenURL={s => dispatch(shortenURL(s))} + handleUpdateShortURL={s => dispatch(updateShortURL(s))} + queryString={queryString} + shortURL={props.shortURL} + isSicp={isSicpEditor} + key="share" + /> + ); + }, [dispatch, isSicpEditor, props.initialEditorValueHash, props.queryString, props.shortURL]); + + const toggleFolderModeButton = React.useMemo(() => { + return ( + dispatch(toggleFolderMode(workspaceLocation))} + key="folder" + /> + ); + }, [ + dispatch, + githubSaveInfo.repoName, + isFolderModeEnabled, + persistenceFile, + props.editorSessionId, + workspaceLocation + ]); + + const playgroundIntroductionTab: SideContentTab = React.useMemo( + () => + makeIntroductionTabFrom( + generateSourceIntroduction(props.playgroundSourceChapter, props.playgroundSourceVariant) + ), + [props.playgroundSourceChapter, props.playgroundSourceVariant] + ); + + React.useEffect(() => { + // TODO: To migrate the state logic away from playgroundSourceChapter + // and playgroundSourceVariant into the language config instead + const languageConfigToSet = getLanguageConfig( + props.playgroundSourceChapter, + props.playgroundSourceVariant + ); + // Hardcoded for Playground only for now, while we await workspace refactoring + // to decouple the SicpWorkspace from the Playground. + dispatch(playgroundConfigLanguage(languageConfigToSet)); + }, [dispatch, props.playgroundSourceChapter, props.playgroundSourceVariant]); + + const languageConfig: SALanguage = useTypedSelector(state => state.playground.languageConfig); + const shouldShowDataVisualizer = languageConfig.supports.dataVisualizer ?? false; + const shouldShowEnvVisualizer = languageConfig.supports.envVisualizer ?? false; + const shouldShowSubstVisualizer = languageConfig.supports.substVisualizer ?? false; + + const tabs = React.useMemo(() => { + const tabs: SideContentTab[] = [playgroundIntroductionTab]; + + const currentLang = props.playgroundSourceChapter; + + if (currentLang === Chapter.HTML) { + // For HTML Chapter, HTML Display tab is added only after code is run + if (props.output.length > 0 && props.output[0].type === 'result') { + tabs.push( + makeHtmlDisplayTabFrom(props.output[0] as ResultOutput, errorMsg => + dispatch(addHtmlConsoleError(errorMsg, workspaceLocation)) + ) + ); + } + return tabs; + } + + if (!usingRemoteExecution) { + // Don't show the following when using remote execution + if (shouldShowDataVisualizer) { + tabs.push(dataVisualizerTab); + } + if (shouldShowEnvVisualizer) { + tabs.push(makeEnvVisualizerTabFrom(workspaceLocation)); + } + if (shouldShowSubstVisualizer) { + tabs.push(makeSubstVisualizerTabFrom(props.output)); + } + } + + if (!isSicpEditor) { + tabs.push(remoteExecutionTab); + } + + return tabs; + }, [ + playgroundIntroductionTab, + props.playgroundSourceChapter, + props.output, + usingRemoteExecution, + isSicpEditor, + dispatch, + workspaceLocation, + shouldShowDataVisualizer, + shouldShowEnvVisualizer, + shouldShowSubstVisualizer, + remoteExecutionTab + ]); + + // Remove Intro and Remote Execution tabs for mobile + const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id))); + + const onLoadMethod = React.useCallback( + (editor: Ace.Editor) => { + const addFold = () => { + editor.getSession().addFold(' ', new Range(1, 0, props.prependLength!, 0)); + editor.renderer.off('afterRender', addFold); + }; + + editor.renderer.on('afterRender', addFold); + }, + [props.prependLength] + ); + + const onChangeMethod = React.useCallback( + (newCode: string, delta: CodeDelta) => { + const input: Input = { + time: Date.now(), + type: 'codeDelta', + data: delta + }; + + pushLog(input); + dispatch(toggleUpdateEnv(true, workspaceLocation)); + dispatch(setEditorHighlightedLines(workspaceLocation, 0, [])); + }, + [pushLog, dispatch, workspaceLocation] + ); + + const onCursorChangeMethod = React.useCallback( + (selection: any) => { + const input: Input = { + time: Date.now(), + type: 'cursorPositionChange', + data: selection.getCursor() + }; + + pushLog(input); + }, + [pushLog] + ); + + const onSelectionChangeMethod = React.useCallback( + (selection: any) => { + const range: SelectionRange = selection.getRange(); + const isBackwards: boolean = selection.isBackwards(); + if (!isEqual(range.start, range.end)) { + const input: Input = { + time: Date.now(), + type: 'selectionRangeData', + data: { range, isBackwards } + }; + + pushLog(input); + } + }, + [pushLog] + ); + + const handleEditorUpdateBreakpoints = React.useCallback( + (editorTabIndex: number, breakpoints: string[]) => { + // get rid of holes in array + const numberOfBreakpoints = breakpoints.filter(arrayItem => !!arrayItem).length; + if (numberOfBreakpoints > 0) { + setHasBreakpoints(true); + if (propsRef.current.playgroundSourceChapter <= 2) { + /** + * There are breakpoints set on Source Chapter 2, so we set the + * Redux state for the editor to evaluate to the substituter + */ + + propsRef.current.handleUsingSubst(true); + } + } + if (numberOfBreakpoints === 0) { + setHasBreakpoints(false); + + if (selectedTab !== SideContentType.substVisualizer) { + propsRef.current.handleReplOutputClear(); + propsRef.current.handleUsingSubst(false); + } + } + propsRef.current.handleEditorUpdateBreakpoints(editorTabIndex, breakpoints); + dispatch(toggleUpdateEnv(true, workspaceLocation)); + }, + [selectedTab, dispatch, workspaceLocation] + ); + + const replDisabled = + props.playgroundSourceChapter === Chapter.HTML || + props.playgroundSourceVariant === Variant.CONCURRENT || + usingRemoteExecution; + + const setActiveEditorTabIndex = React.useCallback( + (activeEditorTabIndex: number | null) => + dispatch(updateActiveEditorTabIndex(workspaceLocation, activeEditorTabIndex)), + [dispatch, workspaceLocation] + ); + const removeEditorTabByIndex = React.useCallback( + (editorTabIndex: number) => dispatch(removeEditorTab(workspaceLocation, editorTabIndex)), + [dispatch, workspaceLocation] + ); + + const editorContainerProps: NormalEditorContainerProps = { + ..._.pick(props, 'editorSessionId', 'isEditorAutorun'), + editorVariant: 'normal', + baseFilePath: WORKSPACE_BASE_PATHS[workspaceLocation], + isFolderModeEnabled, + activeEditorTabIndex, + setActiveEditorTabIndex, + removeEditorTabByIndex, + editorTabs: props.editorTabs.map(convertEditorTabStateToProps), + handleDeclarationNavigate: React.useCallback( + (cursorPosition: Position) => + dispatch(navigateToDeclaration(workspaceLocation, cursorPosition)), + [dispatch, workspaceLocation] + ), + handleEditorEval: memoizedHandlers.handleEditorEval, + handlePromptAutocomplete: React.useCallback( + (row: number, col: number, callback: any) => + dispatch(promptAutocomplete(workspaceLocation, row, col, callback)), + [dispatch, workspaceLocation] + ), + handleSendReplInputToOutput: React.useCallback( + (code: string) => dispatch(sendReplInputToOutput(code, workspaceLocation)), + [dispatch, workspaceLocation] + ), + handleSetSharedbConnected: React.useCallback( + (connected: boolean) => dispatch(setSharedbConnected(workspaceLocation, connected)), + [dispatch, workspaceLocation] + ), + onChange: onChangeMethod, + onCursorChange: onCursorChangeMethod, + onSelectionChange: onSelectionChangeMethod, + onLoad: isSicpEditor && props.prependLength ? onLoadMethod : undefined, + sourceChapter: props.playgroundSourceChapter, + externalLibraryName, + sourceVariant: props.playgroundSourceVariant, + handleEditorValueChange: onEditorValueChange, + handleEditorUpdateBreakpoints: handleEditorUpdateBreakpoints + }; + + const replProps = { + ..._.pick(props, 'output', 'replValue', 'handleReplEval', 'usingSubst'), + handleBrowseHistoryDown: React.useCallback( + () => dispatch(browseReplHistoryDown(workspaceLocation)), + [dispatch, workspaceLocation] + ), + handleBrowseHistoryUp: React.useCallback( + () => dispatch(browseReplHistoryUp(workspaceLocation)), + [dispatch, workspaceLocation] + ), + handleReplValueChange: React.useCallback( + (newValue: string) => dispatch(updateReplValue(newValue, workspaceLocation)), + [dispatch, workspaceLocation] + ), + sourceChapter: props.playgroundSourceChapter, + sourceVariant: props.playgroundSourceVariant, + externalLibrary: ExternalLibraryName.NONE, // temporary placeholder as we phase out libraries + hidden: + selectedTab === SideContentType.substVisualizer || + selectedTab === SideContentType.envVisualizer, + inputHidden: replDisabled, + replButtons: [replDisabled ? null : evalButton, clearButton], + disableScrolling: isSicpEditor + }; + + const sideBarProps: { tabs: SideBarTab[] } = React.useMemo(() => { + // The sidebar is rendered if and only if there is at least one tab present. + // Because whether the sidebar is rendered or not affects the sidebar resizing + // logic, we cannot defer the decision on which sidebar tabs should be rendered + // to the sidebar as it would be too late - the sidebar resizing logic in the + // workspace would not be able to act on that information. Instead, we need to + // determine which sidebar tabs should be rendered here. + return { + tabs: [ + ...(isFolderModeEnabled + ? [ + { + label: 'Folder', + body: ( + + ), + iconName: IconNames.FOLDER_CLOSE, + id: SideContentType.folder + } + ] + : []) + ] + }; + }, [isFolderModeEnabled, workspaceLocation]); + + const workspaceProps: WorkspaceProps = { + controlBarProps: { + editorButtons: [ + autorunButtons, + props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, + chapterSelect, + isSicpEditor ? null : sessionButtons, + languageConfig.supports.multiFile ? toggleFolderModeButton : null, + persistenceButtons, + githubButtons, + usingRemoteExecution || !isSourceLanguage(props.playgroundSourceChapter) + ? null + : props.usingSubst + ? stepperStepLimit + : executionTime + ] + }, + editorContainerProps: editorContainerProps, + handleSideContentHeightChange: React.useCallback( + change => dispatch(changeSideContentHeight(change, workspaceLocation)), + [dispatch, workspaceLocation] + ), + replProps: replProps, + sideBarProps: sideBarProps, + sideContentHeight: props.sideContentHeight, + sideContentProps: { + selectedTabId: selectedTab, + onChange: onChangeTabs, + tabs: { + beforeDynamicTabs: tabs, + afterDynamicTabs: [] + }, + workspaceLocation: isSicpEditor ? 'sicp' : 'playground', + sideContentHeight: props.sideContentHeight + }, + sideContentIsResizeable: selectedTab !== SideContentType.substVisualizer + }; + + const mobileWorkspaceProps: MobileWorkspaceProps = { + editorContainerProps: editorContainerProps, + replProps: replProps, + sideBarProps: sideBarProps, + mobileSideContentProps: { + mobileControlBarProps: { + editorButtons: [ + autorunButtons, + chapterSelect, + props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, + isSicpEditor ? null : sessionButtons, + languageConfig.supports.multiFile ? toggleFolderModeButton : null, + persistenceButtons, + githubButtons + ] + }, + selectedTabId: selectedTab, + onChange: onChangeTabs, + tabs: { + beforeDynamicTabs: mobileTabs, + afterDynamicTabs: [] + }, + workspaceLocation: isSicpEditor ? 'sicp' : 'playground' + } + }; + + return isMobileBreakpoint ? ( +
+ +
+ ) : ( + + + + ); +}; + +export default SicpWorkspace; diff --git a/src/pages/sicp/subcomponents/SicpWorkspaceContainer.tsx b/src/pages/sicp/subcomponents/SicpWorkspaceContainer.tsx index 19b0ba7056..6e65d82cb8 100644 --- a/src/pages/sicp/subcomponents/SicpWorkspaceContainer.tsx +++ b/src/pages/sicp/subcomponents/SicpWorkspaceContainer.tsx @@ -18,7 +18,7 @@ import { updateEditorValue } from '../../../commons/workspace/WorkspaceActions'; import { WorkspaceLocation } from '../../../commons/workspace/WorkspaceTypes'; -import Playground, { DispatchProps, StateProps } from '../../playground/Playground'; +import SicpWorkspace, { DispatchProps, StateProps } from './SicpWorkspace'; const mapStateToProps: MapStateToProps = state => ({ ..._.pick( @@ -78,7 +78,7 @@ const mapDispatchToProps: MapDispatchToProps = (dispatch: Dis */ const SicpWorkspaceContainer = (props: any) => { // FIXME: Remove any - const Component = withRouter(connect(mapStateToProps, mapDispatchToProps)(Playground)); + const Component = withRouter(connect(mapStateToProps, mapDispatchToProps)(SicpWorkspace)); return ; }; From 3104d66c247f590428cfe1ae32ad4c3197a3a4cc Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 18 May 2023 12:24:04 +0800 Subject: [PATCH 02/12] Remove isSicpEditor prop from SicpWorkspace Replaces isSicpEditor with the literal `true` and simplify the conditionals that involve it. This results in removal of features such as the remote execution tab and the shared session buttons. --- src/pages/sicp/subcomponents/CodeSnippet.tsx | 1 - .../sicp/subcomponents/SicpWorkspace.tsx | 117 +++++------------- 2 files changed, 29 insertions(+), 89 deletions(-) diff --git a/src/pages/sicp/subcomponents/CodeSnippet.tsx b/src/pages/sicp/subcomponents/CodeSnippet.tsx index 9bf41cccc7..e2dd994cfa 100644 --- a/src/pages/sicp/subcomponents/CodeSnippet.tsx +++ b/src/pages/sicp/subcomponents/CodeSnippet.tsx @@ -55,7 +55,6 @@ const CodeSnippet: React.FC = props => { const WorkspaceProps = { initialEditorValueHash: props.initialEditorValueHash, prependLength: props.prependLength, - isSicpEditor: true, handleCloseEditor: handleClose }; diff --git a/src/pages/sicp/subcomponents/SicpWorkspace.tsx b/src/pages/sicp/subcomponents/SicpWorkspace.tsx index 43371f0451..4d13ec0acd 100644 --- a/src/pages/sicp/subcomponents/SicpWorkspace.tsx +++ b/src/pages/sicp/subcomponents/SicpWorkspace.tsx @@ -9,7 +9,7 @@ import _, { isEqual } from 'lodash'; import { decompressFromEncodedURIComponent } from 'lz-string'; import * as React from 'react'; import { HotKeys } from 'react-hotkeys'; -import { useDispatch, useStore } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { RouteComponentProps, useHistory, useLocation } from 'react-router'; import { AnyAction, Dispatch } from 'redux'; import { @@ -23,10 +23,7 @@ import { logoutGitHub, logoutGoogle } from 'src/commons/application/actions/SessionActions'; -import { - setEditorSessionId, - setSharedbConnected -} from 'src/commons/collabEditing/CollabEditingActions'; +import { setSharedbConnected } from 'src/commons/collabEditing/CollabEditingActions'; import { useResponsive, useTypedSelector } from 'src/commons/utils/Hooks'; import { showFullJSWarningOnUrlLoad, @@ -81,7 +78,6 @@ import { makeEnvVisualizerTabFrom, makeHtmlDisplayTabFrom, makeIntroductionTabFrom, - makeRemoteExecutionTabFrom, makeSubstVisualizerTabFrom, mobileOnlyTabIds } from 'src/pages/playground/PlaygroundTabs'; @@ -91,7 +87,6 @@ import { getLanguageConfig, InterpreterOutput, isSourceLanguage, - OverallState, ResultOutput, SALanguage } from '../../../commons/application/ApplicationTypes'; @@ -102,7 +97,6 @@ import { ControlBarClearButton } from '../../../commons/controlBar/ControlBarCle import { ControlBarEvalButton } from '../../../commons/controlBar/ControlBarEvalButton'; import { ControlBarExecutionTime } from '../../../commons/controlBar/ControlBarExecutionTime'; import { ControlBarGoogleDriveButtons } from '../../../commons/controlBar/ControlBarGoogleDriveButtons'; -import { ControlBarSessionButtons } from '../../../commons/controlBar/ControlBarSessionButton'; import { ControlBarShareButton } from '../../../commons/controlBar/ControlBarShareButton'; import { ControlBarStepLimit } from '../../../commons/controlBar/ControlBarStepLimit'; import { ControlBarToggleFolderModeButton } from '../../../commons/controlBar/ControlBarToggleFolderModeButton'; @@ -142,7 +136,6 @@ export type PlaygroundProps = OwnProps & }; export type OwnProps = { - isSicpEditor?: boolean; initialEditorValueHash?: string; prependLength?: number; handleCloseEditor?: () => void; @@ -274,7 +267,6 @@ const SicpWorkspace: React.FC = ({ workspaceLocation = 'playground', ...props }) => { - const { isSicpEditor } = props; const { isMobileBreakpoint } = useResponsive(); const propsRef = React.useRef(props); propsRef.current = props; @@ -284,7 +276,6 @@ const SicpWorkspace: React.FC = ({ const [deviceSecret, setDeviceSecret] = React.useState(); const location = useLocation(); const history = useHistory(); - const store = useStore(); const searchParams = new URLSearchParams(location.search); const shouldAddDevice = searchParams.get('add_device'); @@ -314,13 +305,6 @@ const SicpWorkspace: React.FC = ({ }) ); - const remoteExecutionTab: SideContentTab = React.useMemo( - () => makeRemoteExecutionTabFrom(deviceSecret, setDeviceSecret), - [deviceSecret] - ); - - const usingRemoteExecution = - useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; // this is still used by remote execution (EV3) // specifically, for the editor Ctrl+B to work const externalLibraryName = useTypedSelector( @@ -338,7 +322,7 @@ const SicpWorkspace: React.FC = ({ ); }, [props.editorSessionId]); - const hash = isSicpEditor ? props.initialEditorValueHash : props.location.hash; + const hash = props.initialEditorValueHash; React.useEffect(() => { if (!hash) { @@ -477,10 +461,10 @@ const SicpWorkspace: React.FC = ({ isEditorAutorun={props.isEditorAutorun} isRunning={props.isRunning} key="autorun" - autorunDisabled={usingRemoteExecution} + autorunDisabled={false} sourceChapter={props.playgroundSourceChapter} // Disable pause for non-Source languages since they cannot be paused - pauseDisabled={usingRemoteExecution || !isSourceLanguage(props.playgroundSourceChapter)} + pauseDisabled={!isSourceLanguage(props.playgroundSourceChapter)} {...memoizedHandlers} /> ); @@ -490,8 +474,7 @@ const SicpWorkspace: React.FC = ({ props.isEditorAutorun, props.isRunning, props.playgroundSourceChapter, - memoizedHandlers, - usingRemoteExecution + memoizedHandlers ]); const chapterSelectHandler = React.useCallback( @@ -530,15 +513,14 @@ const SicpWorkspace: React.FC = ({ sourceChapter={props.playgroundSourceChapter} sourceVariant={props.playgroundSourceVariant} key="chapter" - disabled={usingRemoteExecution} + disabled={false} /> ), [ chapterSelectHandler, isFolderModeEnabled, props.playgroundSourceChapter, - props.playgroundSourceVariant, - usingRemoteExecution + props.playgroundSourceVariant ] ); @@ -642,37 +624,8 @@ const SicpWorkspace: React.FC = ({ [dispatch, props.stepLimit, workspaceLocation] ); - const getEditorValue = React.useCallback( - // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - () => store.getState().workspaces[workspaceLocation].editorTabs[0].value, - [store, workspaceLocation] - ); - - const sessionButtons = React.useMemo( - () => ( - dispatch(setEditorSessionId(workspaceLocation, id))} - sharedbConnected={props.sharedbConnected} - key="session" - /> - ), - [ - dispatch, - getEditorValue, - isFolderModeEnabled, - props.editorSessionId, - props.sharedbConnected, - workspaceLocation - ] - ); - const shareButton = React.useMemo(() => { - const queryString = isSicpEditor - ? Links.playground + '#' + props.initialEditorValueHash - : props.queryString; + const queryString = Links.playground + '#' + props.initialEditorValueHash; return ( dispatch(generateLzString())} @@ -680,11 +633,11 @@ const SicpWorkspace: React.FC = ({ handleUpdateShortURL={s => dispatch(updateShortURL(s))} queryString={queryString} shortURL={props.shortURL} - isSicp={isSicpEditor} + isSicp key="share" /> ); - }, [dispatch, isSicpEditor, props.initialEditorValueHash, props.queryString, props.shortURL]); + }, [dispatch, props.initialEditorValueHash, props.shortURL]); const toggleFolderModeButton = React.useMemo(() => { return ( @@ -726,9 +679,9 @@ const SicpWorkspace: React.FC = ({ }, [dispatch, props.playgroundSourceChapter, props.playgroundSourceVariant]); const languageConfig: SALanguage = useTypedSelector(state => state.playground.languageConfig); - const shouldShowDataVisualizer = languageConfig.supports.dataVisualizer ?? false; - const shouldShowEnvVisualizer = languageConfig.supports.envVisualizer ?? false; - const shouldShowSubstVisualizer = languageConfig.supports.substVisualizer ?? false; + const shouldShowDataVisualizer = languageConfig.supports.dataVisualizer; + const shouldShowEnvVisualizer = languageConfig.supports.envVisualizer; + const shouldShowSubstVisualizer = languageConfig.supports.substVisualizer; const tabs = React.useMemo(() => { const tabs: SideContentTab[] = [playgroundIntroductionTab]; @@ -747,21 +700,15 @@ const SicpWorkspace: React.FC = ({ return tabs; } - if (!usingRemoteExecution) { - // Don't show the following when using remote execution - if (shouldShowDataVisualizer) { - tabs.push(dataVisualizerTab); - } - if (shouldShowEnvVisualizer) { - tabs.push(makeEnvVisualizerTabFrom(workspaceLocation)); - } - if (shouldShowSubstVisualizer) { - tabs.push(makeSubstVisualizerTabFrom(props.output)); - } + // Don't show the following when using remote execution + if (shouldShowDataVisualizer) { + tabs.push(dataVisualizerTab); } - - if (!isSicpEditor) { - tabs.push(remoteExecutionTab); + if (shouldShowEnvVisualizer) { + tabs.push(makeEnvVisualizerTabFrom(workspaceLocation)); + } + if (shouldShowSubstVisualizer) { + tabs.push(makeSubstVisualizerTabFrom(props.output)); } return tabs; @@ -769,14 +716,11 @@ const SicpWorkspace: React.FC = ({ playgroundIntroductionTab, props.playgroundSourceChapter, props.output, - usingRemoteExecution, - isSicpEditor, dispatch, workspaceLocation, shouldShowDataVisualizer, shouldShowEnvVisualizer, - shouldShowSubstVisualizer, - remoteExecutionTab + shouldShowSubstVisualizer ]); // Remove Intro and Remote Execution tabs for mobile @@ -870,8 +814,7 @@ const SicpWorkspace: React.FC = ({ const replDisabled = props.playgroundSourceChapter === Chapter.HTML || - props.playgroundSourceVariant === Variant.CONCURRENT || - usingRemoteExecution; + props.playgroundSourceVariant === Variant.CONCURRENT; const setActiveEditorTabIndex = React.useCallback( (activeEditorTabIndex: number | null) => @@ -914,7 +857,7 @@ const SicpWorkspace: React.FC = ({ onChange: onChangeMethod, onCursorChange: onCursorChangeMethod, onSelectionChange: onSelectionChangeMethod, - onLoad: isSicpEditor && props.prependLength ? onLoadMethod : undefined, + onLoad: props.prependLength ? onLoadMethod : undefined, sourceChapter: props.playgroundSourceChapter, externalLibraryName, sourceVariant: props.playgroundSourceVariant, @@ -944,7 +887,7 @@ const SicpWorkspace: React.FC = ({ selectedTab === SideContentType.envVisualizer, inputHidden: replDisabled, replButtons: [replDisabled ? null : evalButton, clearButton], - disableScrolling: isSicpEditor + disableScrolling: true }; const sideBarProps: { tabs: SideBarTab[] } = React.useMemo(() => { @@ -981,11 +924,10 @@ const SicpWorkspace: React.FC = ({ autorunButtons, props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, chapterSelect, - isSicpEditor ? null : sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons, - usingRemoteExecution || !isSourceLanguage(props.playgroundSourceChapter) + !isSourceLanguage(props.playgroundSourceChapter) ? null : props.usingSubst ? stepperStepLimit @@ -1007,7 +949,7 @@ const SicpWorkspace: React.FC = ({ beforeDynamicTabs: tabs, afterDynamicTabs: [] }, - workspaceLocation: isSicpEditor ? 'sicp' : 'playground', + workspaceLocation: 'sicp', sideContentHeight: props.sideContentHeight }, sideContentIsResizeable: selectedTab !== SideContentType.substVisualizer @@ -1023,7 +965,6 @@ const SicpWorkspace: React.FC = ({ autorunButtons, chapterSelect, props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, - isSicpEditor ? null : sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons @@ -1035,7 +976,7 @@ const SicpWorkspace: React.FC = ({ beforeDynamicTabs: mobileTabs, afterDynamicTabs: [] }, - workspaceLocation: isSicpEditor ? 'sicp' : 'playground' + workspaceLocation: 'sicp' } }; From 2243494a09c895ada1cffed1aeb41484fdbad638 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 18 May 2023 12:31:13 +0800 Subject: [PATCH 03/12] Remove isSicpEditor prop from Playground Replaces isSicpEditor with the literal `false` and simplify the conditionals that involve it. --- src/pages/playground/Playground.tsx | 45 +++++++---------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index 364857e736..e62afa9c74 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -1,7 +1,6 @@ import { Classes } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Octokit } from '@octokit/rest'; -import { Ace, Range } from 'ace-builds'; import { FSModule } from 'browserfs/dist/node/core/FS'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; @@ -109,7 +108,6 @@ import MobileWorkspace, { } from '../../commons/mobileWorkspace/MobileWorkspace'; import { SideBarTab } from '../../commons/sideBar/SideBar'; import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; -import { Links } from '../../commons/utils/Constants'; import { generateSourceIntroduction } from '../../commons/utils/IntroductionHelper'; import { convertParamToBoolean, convertParamToInt } from '../../commons/utils/ParamParseHelper'; import { IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper'; @@ -142,7 +140,6 @@ export type PlaygroundProps = OwnProps & }; export type OwnProps = { - isSicpEditor?: boolean; initialEditorValueHash?: string; prependLength?: number; handleCloseEditor?: () => void; @@ -271,7 +268,6 @@ export async function handleHash( } const Playground: React.FC = ({ workspaceLocation = 'playground', ...props }) => { - const { isSicpEditor } = props; const { isMobileBreakpoint } = useResponsive(); const propsRef = React.useRef(props); propsRef.current = props; @@ -316,8 +312,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground [deviceSecret] ); - const usingRemoteExecution = - useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; + const usingRemoteExecution = useTypedSelector(state => !!state.session.remoteExecutionSession); // this is still used by remote execution (EV3) // specifically, for the editor Ctrl+B to work const externalLibraryName = useTypedSelector( @@ -335,7 +330,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground ); }, [props.editorSessionId]); - const hash = isSicpEditor ? props.initialEditorValueHash : props.location.hash; + const hash = props.location.hash; React.useEffect(() => { if (!hash) { @@ -667,9 +662,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground ); const shareButton = React.useMemo(() => { - const queryString = isSicpEditor - ? Links.playground + '#' + props.initialEditorValueHash - : props.queryString; + const queryString = props.queryString; return ( dispatch(generateLzString())} @@ -677,11 +670,11 @@ const Playground: React.FC = ({ workspaceLocation = 'playground handleUpdateShortURL={s => dispatch(updateShortURL(s))} queryString={queryString} shortURL={props.shortURL} - isSicp={isSicpEditor} + isSicp={false} key="share" /> ); - }, [dispatch, isSicpEditor, props.initialEditorValueHash, props.queryString, props.shortURL]); + }, [dispatch, props.queryString, props.shortURL]); const toggleFolderModeButton = React.useMemo(() => { return ( @@ -757,9 +750,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground } } - if (!isSicpEditor) { - tabs.push(remoteExecutionTab); - } + tabs.push(remoteExecutionTab); return tabs; }, [ @@ -767,7 +758,6 @@ const Playground: React.FC = ({ workspaceLocation = 'playground props.playgroundSourceChapter, props.output, usingRemoteExecution, - isSicpEditor, dispatch, workspaceLocation, shouldShowDataVisualizer, @@ -779,18 +769,6 @@ const Playground: React.FC = ({ workspaceLocation = 'playground // Remove Intro and Remote Execution tabs for mobile const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id))); - const onLoadMethod = React.useCallback( - (editor: Ace.Editor) => { - const addFold = () => { - editor.getSession().addFold(' ', new Range(1, 0, props.prependLength!, 0)); - editor.renderer.off('afterRender', addFold); - }; - - editor.renderer.on('afterRender', addFold); - }, - [props.prependLength] - ); - const onChangeMethod = React.useCallback( (newCode: string, delta: CodeDelta) => { const input: Input = { @@ -911,7 +889,6 @@ const Playground: React.FC = ({ workspaceLocation = 'playground onChange: onChangeMethod, onCursorChange: onCursorChangeMethod, onSelectionChange: onSelectionChangeMethod, - onLoad: isSicpEditor && props.prependLength ? onLoadMethod : undefined, sourceChapter: props.playgroundSourceChapter, externalLibraryName, sourceVariant: props.playgroundSourceVariant, @@ -941,7 +918,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground selectedTab === SideContentType.envVisualizer, inputHidden: replDisabled, replButtons: [replDisabled ? null : evalButton, clearButton], - disableScrolling: isSicpEditor + disableScrolling: false }; const sideBarProps: { tabs: SideBarTab[] } = React.useMemo(() => { @@ -978,7 +955,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground autorunButtons, props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, chapterSelect, - isSicpEditor ? null : sessionButtons, + sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons, @@ -1004,7 +981,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground beforeDynamicTabs: tabs, afterDynamicTabs: [] }, - workspaceLocation: isSicpEditor ? 'sicp' : 'playground', + workspaceLocation: 'playground', sideContentHeight: props.sideContentHeight }, sideContentIsResizeable: selectedTab !== SideContentType.substVisualizer @@ -1020,7 +997,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground autorunButtons, chapterSelect, props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, - isSicpEditor ? null : sessionButtons, + sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons @@ -1032,7 +1009,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground beforeDynamicTabs: mobileTabs, afterDynamicTabs: [] }, - workspaceLocation: isSicpEditor ? 'sicp' : 'playground' + workspaceLocation: 'playground' } }; From 8531c985292f0a2d39436d67a835a95907210017 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 18 May 2023 12:36:18 +0800 Subject: [PATCH 04/12] Remove remote execution logic from SicpWorkspace Removes from SicpWorkspace remaining query string handling logic that was only used for remote execution (`shouldAddDevice` param). --- .../sicp/subcomponents/SicpWorkspace.tsx | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/pages/sicp/subcomponents/SicpWorkspace.tsx b/src/pages/sicp/subcomponents/SicpWorkspace.tsx index 4d13ec0acd..f0cd66f94e 100644 --- a/src/pages/sicp/subcomponents/SicpWorkspace.tsx +++ b/src/pages/sicp/subcomponents/SicpWorkspace.tsx @@ -10,7 +10,7 @@ import { decompressFromEncodedURIComponent } from 'lz-string'; import * as React from 'react'; import { HotKeys } from 'react-hotkeys'; import { useDispatch } from 'react-redux'; -import { RouteComponentProps, useHistory, useLocation } from 'react-router'; +import { RouteComponentProps } from 'react-router'; import { AnyAction, Dispatch } from 'redux'; import { beginDebuggerPause, @@ -273,29 +273,14 @@ const SicpWorkspace: React.FC = ({ const dispatch = useDispatch(); - const [deviceSecret, setDeviceSecret] = React.useState(); - const location = useLocation(); - const history = useHistory(); - const searchParams = new URLSearchParams(location.search); - const shouldAddDevice = searchParams.get('add_device'); - const { isFolderModeEnabled, activeEditorTabIndex } = useTypedSelector( state => state.workspaces[workspaceLocation] ); const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); - // Hide search query from URL to maintain an illusion of security. The device secret - // is still exposed via the 'Referer' header when requesting external content (e.g. Google API fonts) - if (shouldAddDevice && !deviceSecret) { - setDeviceSecret(shouldAddDevice); - history.replace(location.pathname); - } - const [lastEdit, setLastEdit] = React.useState(new Date()); const [isGreen, setIsGreen] = React.useState(false); - const [selectedTab, setSelectedTab] = React.useState( - shouldAddDevice ? SideContentType.remoteExecution : SideContentType.introduction - ); + const [selectedTab, setSelectedTab] = React.useState(SideContentType.introduction); const [hasBreakpoints, setHasBreakpoints] = React.useState(false); const [sessionId, setSessionId] = React.useState(() => initSession('playground', { @@ -700,7 +685,6 @@ const SicpWorkspace: React.FC = ({ return tabs; } - // Don't show the following when using remote execution if (shouldShowDataVisualizer) { tabs.push(dataVisualizerTab); } @@ -723,7 +707,7 @@ const SicpWorkspace: React.FC = ({ shouldShowSubstVisualizer ]); - // Remove Intro and Remote Execution tabs for mobile + // Remove Intro tab for mobile const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id))); const onLoadMethod = React.useCallback( From 01b6a4ce759b9710d4e0588f810040365dbfe72d Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 25 May 2023 01:06:18 +0800 Subject: [PATCH 05/12] Copy over Playground changes post-merge --- .../sicp/subcomponents/SicpWorkspace.tsx | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/src/pages/sicp/subcomponents/SicpWorkspace.tsx b/src/pages/sicp/subcomponents/SicpWorkspace.tsx index f0cd66f94e..1ebd646b7b 100644 --- a/src/pages/sicp/subcomponents/SicpWorkspace.tsx +++ b/src/pages/sicp/subcomponents/SicpWorkspace.tsx @@ -114,7 +114,7 @@ import MobileWorkspace, { import { SideBarTab } from '../../../commons/sideBar/SideBar'; import { SideContentTab, SideContentType } from '../../../commons/sideContent/SideContentTypes'; import { Links } from '../../../commons/utils/Constants'; -import { generateSourceIntroduction } from '../../../commons/utils/IntroductionHelper'; +import { generateLanguageIntroduction } from '../../../commons/utils/IntroductionHelper'; import { convertParamToBoolean, convertParamToInt } from '../../../commons/utils/ParamParseHelper'; import { IParsedQuery, parseQuery } from '../../../commons/utils/QueryHelper'; import Workspace, { WorkspaceProps } from '../../../commons/workspace/Workspace'; @@ -438,6 +438,8 @@ const SicpWorkspace: React.FC = ({ }; }, [dispatch, workspaceLocation]); + const languageConfig: SALanguage = useTypedSelector(state => state.playground.languageConfig); + const autorunButtons = React.useMemo(() => { return ( = ({ isRunning={props.isRunning} key="autorun" autorunDisabled={false} - sourceChapter={props.playgroundSourceChapter} + sourceChapter={languageConfig.chapter} // Disable pause for non-Source languages since they cannot be paused - pauseDisabled={!isSourceLanguage(props.playgroundSourceChapter)} + pauseDisabled={!isSourceLanguage(languageConfig.chapter)} {...memoizedHandlers} /> ); @@ -458,7 +460,7 @@ const SicpWorkspace: React.FC = ({ props.isDebugging, props.isEditorAutorun, props.isRunning, - props.playgroundSourceChapter, + languageConfig.chapter, memoizedHandlers ]); @@ -495,18 +497,13 @@ const SicpWorkspace: React.FC = ({ ), - [ - chapterSelectHandler, - isFolderModeEnabled, - props.playgroundSourceChapter, - props.playgroundSourceVariant - ] + [chapterSelectHandler, isFolderModeEnabled, languageConfig.chapter, languageConfig.variant] ); const clearButton = React.useMemo( @@ -643,14 +640,6 @@ const SicpWorkspace: React.FC = ({ workspaceLocation ]); - const playgroundIntroductionTab: SideContentTab = React.useMemo( - () => - makeIntroductionTabFrom( - generateSourceIntroduction(props.playgroundSourceChapter, props.playgroundSourceVariant) - ), - [props.playgroundSourceChapter, props.playgroundSourceVariant] - ); - React.useEffect(() => { // TODO: To migrate the state logic away from playgroundSourceChapter // and playgroundSourceVariant into the language config instead @@ -663,15 +652,19 @@ const SicpWorkspace: React.FC = ({ dispatch(playgroundConfigLanguage(languageConfigToSet)); }, [dispatch, props.playgroundSourceChapter, props.playgroundSourceVariant]); - const languageConfig: SALanguage = useTypedSelector(state => state.playground.languageConfig); const shouldShowDataVisualizer = languageConfig.supports.dataVisualizer; const shouldShowEnvVisualizer = languageConfig.supports.envVisualizer; const shouldShowSubstVisualizer = languageConfig.supports.substVisualizer; + const playgroundIntroductionTab: SideContentTab = React.useMemo( + () => makeIntroductionTabFrom(generateLanguageIntroduction(languageConfig)), + [languageConfig] + ); + const tabs = React.useMemo(() => { const tabs: SideContentTab[] = [playgroundIntroductionTab]; - const currentLang = props.playgroundSourceChapter; + const currentLang = languageConfig.chapter; if (currentLang === Chapter.HTML) { // For HTML Chapter, HTML Display tab is added only after code is run @@ -698,7 +691,7 @@ const SicpWorkspace: React.FC = ({ return tabs; }, [ playgroundIntroductionTab, - props.playgroundSourceChapter, + languageConfig.chapter, props.output, dispatch, workspaceLocation, @@ -796,9 +789,7 @@ const SicpWorkspace: React.FC = ({ [selectedTab, dispatch, workspaceLocation] ); - const replDisabled = - props.playgroundSourceChapter === Chapter.HTML || - props.playgroundSourceVariant === Variant.CONCURRENT; + const replDisabled = !languageConfig.supports.repl; const setActiveEditorTabIndex = React.useCallback( (activeEditorTabIndex: number | null) => @@ -842,9 +833,9 @@ const SicpWorkspace: React.FC = ({ onCursorChange: onCursorChangeMethod, onSelectionChange: onSelectionChangeMethod, onLoad: props.prependLength ? onLoadMethod : undefined, - sourceChapter: props.playgroundSourceChapter, + sourceChapter: languageConfig.chapter, externalLibraryName, - sourceVariant: props.playgroundSourceVariant, + sourceVariant: languageConfig.variant, handleEditorValueChange: onEditorValueChange, handleEditorUpdateBreakpoints: handleEditorUpdateBreakpoints }; @@ -863,8 +854,8 @@ const SicpWorkspace: React.FC = ({ (newValue: string) => dispatch(updateReplValue(newValue, workspaceLocation)), [dispatch, workspaceLocation] ), - sourceChapter: props.playgroundSourceChapter, - sourceVariant: props.playgroundSourceVariant, + sourceChapter: languageConfig.chapter, + sourceVariant: languageConfig.variant, externalLibrary: ExternalLibraryName.NONE, // temporary placeholder as we phase out libraries hidden: selectedTab === SideContentType.substVisualizer || @@ -906,12 +897,12 @@ const SicpWorkspace: React.FC = ({ controlBarProps: { editorButtons: [ autorunButtons, - props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, + languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, chapterSelect, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons, - !isSourceLanguage(props.playgroundSourceChapter) + !isSourceLanguage(languageConfig.chapter) ? null : props.usingSubst ? stepperStepLimit @@ -948,7 +939,7 @@ const SicpWorkspace: React.FC = ({ editorButtons: [ autorunButtons, chapterSelect, - props.playgroundSourceChapter === Chapter.FULL_JS ? null : shareButton, + languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons From e6771423e3ba2d6aabc59909e24daafe3b40c079 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 15 Jun 2023 02:08:20 +0800 Subject: [PATCH 06/12] Undo removal of isSicpEditor prop from Playground Partially reverts 2243494a09c895ada1cffed1aeb41484fdbad638 in preparation for merging of commits from refactoring change. Done simply to prevent as many merge conflicts as possible. Changes are to be restored later. --- src/pages/playground/Playground.tsx | 45 ++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index 83317689a9..898d5ae39e 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -1,6 +1,7 @@ import { Classes } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Octokit } from '@octokit/rest'; +import { Ace, Range } from 'ace-builds'; import { FSModule } from 'browserfs/dist/node/core/FS'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; @@ -108,6 +109,7 @@ import MobileWorkspace, { } from '../../commons/mobileWorkspace/MobileWorkspace'; import { SideBarTab } from '../../commons/sideBar/SideBar'; import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; +import { Links } from '../../commons/utils/Constants'; import { generateLanguageIntroduction } from '../../commons/utils/IntroductionHelper'; import { convertParamToBoolean, convertParamToInt } from '../../commons/utils/ParamParseHelper'; import { IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper'; @@ -267,6 +269,9 @@ export async function handleHash( } } +// FIXME: Remove this after merging is complete +const isSicpEditor = false; + const Playground: React.FC = ({ workspaceLocation = 'playground', ...props }) => { const { isMobileBreakpoint } = useResponsive(); const propsRef = React.useRef(props); @@ -312,7 +317,8 @@ const Playground: React.FC = ({ workspaceLocation = 'playground [deviceSecret] ); - const usingRemoteExecution = useTypedSelector(state => !!state.session.remoteExecutionSession); + const usingRemoteExecution = + useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; // this is still used by remote execution (EV3) // specifically, for the editor Ctrl+B to work const externalLibraryName = useTypedSelector( @@ -330,7 +336,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground ); }, [props.editorSessionId]); - const hash = props.location.hash; + const hash = isSicpEditor ? props.initialEditorValueHash : props.location.hash; React.useEffect(() => { if (!hash) { @@ -664,7 +670,9 @@ const Playground: React.FC = ({ workspaceLocation = 'playground ); const shareButton = React.useMemo(() => { - const queryString = props.queryString; + const queryString = isSicpEditor + ? Links.playground + '#' + props.initialEditorValueHash + : props.queryString; return ( dispatch(generateLzString())} @@ -672,11 +680,11 @@ const Playground: React.FC = ({ workspaceLocation = 'playground handleUpdateShortURL={s => dispatch(updateShortURL(s))} queryString={queryString} shortURL={props.shortURL} - isSicp={false} + isSicp={isSicpEditor} key="share" /> ); - }, [dispatch, props.queryString, props.shortURL]); + }, [dispatch, props.initialEditorValueHash, props.queryString, props.shortURL]); const toggleFolderModeButton = React.useMemo(() => { return ( @@ -746,7 +754,9 @@ const Playground: React.FC = ({ workspaceLocation = 'playground } } - tabs.push(remoteExecutionTab); + if (!isSicpEditor) { + tabs.push(remoteExecutionTab); + } return tabs; }, [ @@ -765,6 +775,18 @@ const Playground: React.FC = ({ workspaceLocation = 'playground // Remove Intro and Remote Execution tabs for mobile const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id))); + const onLoadMethod = React.useCallback( + (editor: Ace.Editor) => { + const addFold = () => { + editor.getSession().addFold(' ', new Range(1, 0, props.prependLength!, 0)); + editor.renderer.off('afterRender', addFold); + }; + + editor.renderer.on('afterRender', addFold); + }, + [props.prependLength] + ); + const onChangeMethod = React.useCallback( (newCode: string, delta: CodeDelta) => { const input: Input = { @@ -882,6 +904,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground onChange: onChangeMethod, onCursorChange: onCursorChangeMethod, onSelectionChange: onSelectionChangeMethod, + onLoad: isSicpEditor && props.prependLength ? onLoadMethod : undefined, sourceChapter: languageConfig.chapter, externalLibraryName, sourceVariant: languageConfig.variant, @@ -911,7 +934,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground selectedTab === SideContentType.envVisualizer, inputHidden: replDisabled, replButtons: [replDisabled ? null : evalButton, clearButton], - disableScrolling: false + disableScrolling: isSicpEditor }; const sideBarProps: { tabs: SideBarTab[] } = React.useMemo(() => { @@ -948,7 +971,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground autorunButtons, languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, chapterSelect, - sessionButtons, + isSicpEditor ? null : sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons, @@ -974,7 +997,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground beforeDynamicTabs: tabs, afterDynamicTabs: [] }, - workspaceLocation: 'playground', + workspaceLocation: isSicpEditor ? 'sicp' : 'playground', sideContentHeight: props.sideContentHeight }, sideContentIsResizeable: selectedTab !== SideContentType.substVisualizer @@ -990,7 +1013,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground autorunButtons, chapterSelect, languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, - sessionButtons, + isSicpEditor ? null : sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons @@ -1002,7 +1025,7 @@ const Playground: React.FC = ({ workspaceLocation = 'playground beforeDynamicTabs: mobileTabs, afterDynamicTabs: [] }, - workspaceLocation: 'playground' + workspaceLocation: isSicpEditor ? 'sicp' : 'playground' } }; From 89072a176ab6e021b23498f52b65bc8ee2cf340a Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 15 Jun 2023 02:32:56 +0800 Subject: [PATCH 07/12] Migrate over merged changes to SicpWorkspace --- src/pages/sicp/subcomponents/CodeSnippet.tsx | 6 +- .../sicp/subcomponents/SicpWorkspace.tsx | 421 ++++++++++-------- 2 files changed, 227 insertions(+), 200 deletions(-) diff --git a/src/pages/sicp/subcomponents/CodeSnippet.tsx b/src/pages/sicp/subcomponents/CodeSnippet.tsx index 6ba668fc15..3f7a71f89d 100644 --- a/src/pages/sicp/subcomponents/CodeSnippet.tsx +++ b/src/pages/sicp/subcomponents/CodeSnippet.tsx @@ -7,9 +7,9 @@ import ControlBar from 'src/commons/controlBar/ControlBar'; import { ControlBarCloseButton } from 'src/commons/controlBar/ControlBarCloseButton'; import { useResponsive } from 'src/commons/utils/Hooks'; import { SourceTheme } from 'src/features/sicp/SourceTheme'; -import Playground from 'src/pages/playground/Playground'; import { CodeSnippetContext } from '../Sicp'; +import SicpWorkspace from './SicpWorkspace'; export type CodeSnippetProps = OwnProps; type OwnProps = { @@ -80,13 +80,13 @@ const CodeSnippet: React.FC = props => { {isMobileBreakpoint ? (
- +
) : (
- +
diff --git a/src/pages/sicp/subcomponents/SicpWorkspace.tsx b/src/pages/sicp/subcomponents/SicpWorkspace.tsx index 1ebd646b7b..d8d189f936 100644 --- a/src/pages/sicp/subcomponents/SicpWorkspace.tsx +++ b/src/pages/sicp/subcomponents/SicpWorkspace.tsx @@ -1,16 +1,14 @@ import { Classes } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Octokit } from '@octokit/rest'; import { Ace, Range } from 'ace-builds'; import { FSModule } from 'browserfs/dist/node/core/FS'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; -import _, { isEqual } from 'lodash'; +import { isEqual } from 'lodash'; import { decompressFromEncodedURIComponent } from 'lz-string'; -import * as React from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { HotKeys } from 'react-hotkeys'; import { useDispatch } from 'react-redux'; -import { RouteComponentProps } from 'react-router'; import { AnyAction, Dispatch } from 'redux'; import { beginDebuggerPause, @@ -35,25 +33,33 @@ import { addHtmlConsoleError, browseReplHistoryDown, browseReplHistoryUp, + changeExecTime, changeSideContentHeight, changeStepLimit, + chapterSelect, + clearReplOutput, evalEditor, + evalRepl, navigateToDeclaration, promptAutocomplete, removeEditorTab, removeEditorTabsForDirectory, sendReplInputToOutput, + setEditorBreakpoint, setEditorHighlightedLines, setFolderMode, toggleEditorAutorun, toggleFolderMode, toggleUpdateEnv, + toggleUsingEnv, + toggleUsingSubst, updateActiveEditorTabIndex, + updateEditorValue, updateEnvSteps, updateEnvStepsTotal, updateReplValue } from 'src/commons/workspace/WorkspaceActions'; -import { EditorTabState, WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; +import { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes'; import EnvVisualizer from 'src/features/envVisualizer/EnvVisualizer'; import { githubOpenFile, @@ -85,7 +91,6 @@ import { import { getDefaultFilePath, getLanguageConfig, - InterpreterOutput, isSourceLanguage, ResultOutput, SALanguage @@ -119,8 +124,6 @@ import { convertParamToBoolean, convertParamToInt } from '../../../commons/utils import { IParsedQuery, parseQuery } from '../../../commons/utils/QueryHelper'; import Workspace, { WorkspaceProps } from '../../../commons/workspace/Workspace'; import { initSession, log } from '../../../features/eventLogging'; -import { GitHubSaveInfo } from '../../../features/github/GitHubTypes'; -import { PersistenceFile } from '../../../features/persistence/PersistenceTypes'; import { CodeDelta, Input, @@ -128,67 +131,23 @@ import { } from '../../../features/sourceRecorder/SourceRecorderTypes'; import { WORKSPACE_BASE_PATHS } from '../../fileSystem/createInBrowserFileSystem'; -export type PlaygroundProps = OwnProps & - DispatchProps & - StateProps & - RouteComponentProps<{}> & { - workspaceLocation?: WorkspaceLocation; - }; - -export type OwnProps = { +export type PlaygroundProps = { initialEditorValueHash?: string; prependLength?: number; handleCloseEditor?: () => void; }; -export type DispatchProps = { - handleChangeExecTime: (execTime: number) => void; - handleChapterSelect: (chapter: Chapter, variant: Variant) => void; - handleEditorValueChange: (editorTabIndex: number, newEditorValue: string) => void; - handleEditorUpdateBreakpoints: (editorTabIndex: number, newBreakpoints: string[]) => void; - handleReplEval: () => void; - handleReplOutputClear: () => void; - handleUsingEnv: (usingEnv: boolean) => void; - handleUsingSubst: (usingSubst: boolean) => void; -}; - -export type StateProps = { - editorTabs: EditorTabState[]; - programPrependValue: string; - programPostpendValue: string; - editorSessionId: string; - execTime: number; - isEditorAutorun: boolean; - isRunning: boolean; - isDebugging: boolean; - enableDebugging: boolean; - output: InterpreterOutput[]; - queryString?: string; - shortURL?: string; - replValue: string; - sideContentHeight?: number; - playgroundSourceChapter: Chapter; - playgroundSourceVariant: Variant; - courseSourceChapter?: number; - courseSourceVariant?: Variant; - stepLimit: number; - sharedbConnected: boolean; - usingEnv: boolean; - usingSubst: boolean; - persistenceUser: string | undefined; - persistenceFile: PersistenceFile | undefined; - githubOctokitObject: { octokit: Octokit | undefined }; - githubSaveInfo: GitHubSaveInfo; -}; - const keyMap = { goGreen: 'h u l k' }; export async function handleHash( hash: string, - props: PlaygroundProps, + handlers: { + handleChapterSelect: (chapter: Chapter, variant: Variant) => void; + handleChangeExecTime: (execTime: number) => void; + }, workspaceLocation: WorkspaceLocation, dispatch: Dispatch, - fileSystem: FSModule + fileSystem: FSModule | null ) { // Make the parsed query string object a Partial because we might access keys which are not set. const qs: Partial = parseQuery(hash); @@ -217,7 +176,9 @@ export async function handleHash( [defaultFilePath]: program } : parseQuery(decompressFromEncodedURIComponent(qs.files)); - await overwriteFilesInWorkspace(workspaceLocation, fileSystem, files); + if (fileSystem !== null) { + await overwriteFilesInWorkspace(workspaceLocation, fileSystem, files); + } // BrowserFS does not provide a way of listening to changes in the file system, which makes // updating the file system view troublesome. To force the file system view to re-render @@ -250,7 +211,7 @@ export async function handleHash( // TODO: To migrate the state logic away from playgroundSourceChapter // and playgroundSourceVariant into the language config instead const languageConfig = getLanguageConfig(chapter, qs.variant as Variant); - props.handleChapterSelect(chapter, languageConfig.variant); + handlers.handleChapterSelect(chapter, languageConfig.variant); // Hardcoded for Playground only for now, while we await workspace refactoring // to decouple the SicpWorkspace from the Playground. dispatch(playgroundConfigLanguage(languageConfig)); @@ -258,35 +219,78 @@ export async function handleHash( const execTime = Math.max(convertParamToInt(qs.exec || '1000') || 1000, 1000); if (execTime) { - props.handleChangeExecTime(execTime); + handlers.handleChangeExecTime(execTime); } } } -const SicpWorkspace: React.FC = ({ - workspaceLocation = 'playground', - ...props -}) => { +const SicpWorkspace: React.FC = props => { + const workspaceLocation: WorkspaceLocation = 'sicp'; const { isMobileBreakpoint } = useResponsive(); - const propsRef = React.useRef(props); - propsRef.current = props; - const dispatch = useDispatch(); - - const { isFolderModeEnabled, activeEditorTabIndex } = useTypedSelector( - state => state.workspaces[workspaceLocation] - ); + // Selectors and handlers migrated over from deprecated withRouter implementation + const { + editorTabs, + editorSessionId, + execTime, + stepLimit, + isEditorAutorun, + isRunning, + isDebugging, + output, + replValue, + sideContentHeight, + usingSubst, + isFolderModeEnabled, + activeEditorTabIndex, + context: { chapter: playgroundSourceChapter, variant: playgroundSourceVariant } + } = useTypedSelector(state => state.workspaces[workspaceLocation]); const fileSystem = useTypedSelector(state => state.fileSystem.inBrowserFileSystem); + const { shortURL, persistenceFile, githubSaveInfo } = useTypedSelector(state => state.playground); + const { + sourceChapter: courseSourceChapter, + sourceVariant: courseSourceVariant, + googleUser: persistenceUser, + githubOctokitObject + } = useTypedSelector(state => state.session); + + const dispatch = useDispatch(); + const { + handleChangeExecTime, + handleChapterSelect, + handleEditorValueChange, + handleSetEditorBreakpoints, + handleReplEval, + handleReplOutputClear, + handleUsingEnv, + handleUsingSubst + } = useMemo(() => { + return { + handleChangeExecTime: (execTime: number) => + dispatch(changeExecTime(execTime, workspaceLocation)), + handleChapterSelect: (chapter: Chapter, variant: Variant) => + dispatch(chapterSelect(chapter, variant, workspaceLocation)), + handleEditorValueChange: (editorTabIndex: number, newEditorValue: string) => + dispatch(updateEditorValue(workspaceLocation, editorTabIndex, newEditorValue)), + handleSetEditorBreakpoints: (editorTabIndex: number, newBreakpoints: string[]) => + dispatch(setEditorBreakpoint(workspaceLocation, editorTabIndex, newBreakpoints)), + handleReplEval: () => dispatch(evalRepl(workspaceLocation)), + handleReplOutputClear: () => dispatch(clearReplOutput(workspaceLocation)), + handleUsingEnv: (usingEnv: boolean) => dispatch(toggleUsingEnv(usingEnv, workspaceLocation)), + handleUsingSubst: (usingSubst: boolean) => + dispatch(toggleUsingSubst(usingSubst, workspaceLocation)) + }; + }, [dispatch, workspaceLocation]); - const [lastEdit, setLastEdit] = React.useState(new Date()); - const [isGreen, setIsGreen] = React.useState(false); - const [selectedTab, setSelectedTab] = React.useState(SideContentType.introduction); - const [hasBreakpoints, setHasBreakpoints] = React.useState(false); - const [sessionId, setSessionId] = React.useState(() => + const [lastEdit, setLastEdit] = useState(new Date()); + const [isGreen, setIsGreen] = useState(false); + const [selectedTab, setSelectedTab] = useState(SideContentType.introduction); + const [hasBreakpoints, setHasBreakpoints] = useState(false); + const [sessionId, setSessionId] = useState(() => initSession('playground', { // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - editorValue: propsRef.current.editorTabs[0]?.value ?? '', - chapter: propsRef.current.playgroundSourceChapter + editorValue: editorTabs[0]?.value ?? '', + chapter: playgroundSourceChapter }) ); @@ -296,30 +300,28 @@ const SicpWorkspace: React.FC = ({ state => state.workspaces.playground.externalLibrary ); - React.useEffect(() => { + useEffect(() => { // When the editor session Id changes, then treat it as a new session. setSessionId( initSession('playground', { // TODO: Hardcoded to make use of the first editor tab. Rewrite after editor tabs are added. - editorValue: propsRef.current.editorTabs[0]?.value ?? '', - chapter: propsRef.current.playgroundSourceChapter + editorValue: editorTabs[0]?.value ?? '', + chapter: playgroundSourceChapter }) ); - }, [props.editorSessionId]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editorSessionId]); const hash = props.initialEditorValueHash; - React.useEffect(() => { + useEffect(() => { if (!hash) { // If not a accessing via shared link, use the Source chapter and variant in the current course - if (props.courseSourceChapter && props.courseSourceVariant) { - propsRef.current.handleChapterSelect(props.courseSourceChapter, props.courseSourceVariant); + if (courseSourceChapter && courseSourceVariant) { + handleChapterSelect(courseSourceChapter, courseSourceVariant); // TODO: To migrate the state logic away from playgroundSourceChapter // and playgroundSourceVariant into the language config instead - const languageConfig = getLanguageConfig( - props.courseSourceChapter, - props.courseSourceVariant - ); + const languageConfig = getLanguageConfig(courseSourceChapter, courseSourceVariant); // Hardcoded for Playground only for now, while we await workspace refactoring // to decouple the SicpWorkspace from the Playground. dispatch(playgroundConfigLanguage(languageConfig)); @@ -329,22 +331,28 @@ const SicpWorkspace: React.FC = ({ } return; } - if (fileSystem !== null) { - handleHash(hash, propsRef.current, workspaceLocation, dispatch, fileSystem); - } + handleHash( + hash, + { handleChangeExecTime, handleChapterSelect }, + workspaceLocation, + dispatch, + fileSystem + ); }, [ dispatch, fileSystem, hash, - props.courseSourceChapter, - props.courseSourceVariant, - workspaceLocation + courseSourceChapter, + courseSourceVariant, + workspaceLocation, + handleChapterSelect, + handleChangeExecTime ]); /** * Handles toggling of relevant SideContentTabs when mobile breakpoint it hit */ - React.useEffect(() => { + useEffect(() => { if (isMobileBreakpoint && desktopOnlyTabIds.includes(selectedTab)) { setSelectedTab(SideContentType.mobileEditor); } else if (!isMobileBreakpoint && mobileOnlyTabIds.includes(selectedTab)) { @@ -352,29 +360,31 @@ const SicpWorkspace: React.FC = ({ } }, [isMobileBreakpoint, selectedTab]); - const handlers = React.useMemo( + const handlers = useMemo( () => ({ goGreen: () => setIsGreen(!isGreen) }), [isGreen] ); - const onEditorValueChange = React.useCallback((editorTabIndex, newEditorValue) => { - setLastEdit(new Date()); - propsRef.current.handleEditorValueChange(editorTabIndex, newEditorValue); - }, []); + const onEditorValueChange = useCallback( + (editorTabIndex, newEditorValue) => { + setLastEdit(new Date()); + handleEditorValueChange(editorTabIndex, newEditorValue); + }, + [handleEditorValueChange] + ); - const handleEnvVisualiserReset = React.useCallback(() => { - const { handleUsingEnv } = propsRef.current; + const handleEnvVisualiserReset = useCallback(() => { handleUsingEnv(false); EnvVisualizer.clearEnv(); dispatch(updateEnvSteps(-1, workspaceLocation)); dispatch(updateEnvStepsTotal(0, workspaceLocation)); dispatch(toggleUpdateEnv(true, workspaceLocation)); dispatch(setEditorHighlightedLines(workspaceLocation, 0, [])); - }, [dispatch, workspaceLocation]); + }, [dispatch, workspaceLocation, handleUsingEnv]); - const onChangeTabs = React.useCallback( + const onChangeTabs = useCallback( ( newTabId: SideContentType, prevTabId: SideContentType, @@ -392,9 +402,6 @@ const SicpWorkspace: React.FC = ({ return; } - const { handleUsingEnv, handleUsingSubst, handleReplOutputClear, playgroundSourceChapter } = - propsRef.current; - if (newTabId !== SideContentType.envVisualizer) { handleEnvVisualiserReset(); } @@ -417,17 +424,24 @@ const SicpWorkspace: React.FC = ({ setSelectedTab(newTabId); }, - [hasBreakpoints, handleEnvVisualiserReset] + [ + hasBreakpoints, + handleEnvVisualiserReset, + playgroundSourceChapter, + handleUsingSubst, + handleUsingEnv, + handleReplOutputClear + ] ); - const pushLog = React.useCallback( + const pushLog = useCallback( (newInput: Input) => { log(sessionId, newInput); }, [sessionId] ); - const memoizedHandlers = React.useMemo(() => { + const memoizedHandlers = useMemo(() => { return { handleEditorEval: () => dispatch(evalEditor(workspaceLocation)), handleInterruptEval: () => dispatch(beginInterruptExecution(workspaceLocation)), @@ -440,13 +454,13 @@ const SicpWorkspace: React.FC = ({ const languageConfig: SALanguage = useTypedSelector(state => state.playground.languageConfig); - const autorunButtons = React.useMemo(() => { + const autorunButtons = useMemo(() => { return ( = ({ ); }, [ activeEditorTabIndex, - props.isDebugging, - props.isEditorAutorun, - props.isRunning, + isDebugging, + isEditorAutorun, + isRunning, languageConfig.chapter, memoizedHandlers ]); - const chapterSelectHandler = React.useCallback( + const chapterSelectHandler = useCallback( (sublanguage: SALanguage, e: any) => { const { chapter, variant } = sublanguage; - const { handleUsingSubst, handleReplOutputClear, handleChapterSelect } = propsRef.current; if ((chapter <= 2 && hasBreakpoints) || selectedTab === SideContentType.substVisualizer) { handleUsingSubst(true); } @@ -489,10 +502,18 @@ const SicpWorkspace: React.FC = ({ // to decouple the SicpWorkspace from the Playground. dispatch(playgroundConfigLanguage(sublanguage)); }, - [dispatch, hasBreakpoints, selectedTab, pushLog] + [ + dispatch, + hasBreakpoints, + selectedTab, + pushLog, + handleReplOutputClear, + handleUsingSubst, + handleChapterSelect + ] ); - const chapterSelect = React.useMemo( + const chapterSelectButton = useMemo( () => ( = ({ [chapterSelectHandler, isFolderModeEnabled, languageConfig.chapter, languageConfig.variant] ); - const clearButton = React.useMemo( + const clearButton = useMemo( () => selectedTab === SideContentType.substVisualizer ? null : ( - + ), - [props.handleReplOutputClear, selectedTab] + [handleReplOutputClear, selectedTab] ); - const evalButton = React.useMemo( + const evalButton = useMemo( () => selectedTab === SideContentType.substVisualizer ? null : ( ), - [props.handleReplEval, props.isRunning, selectedTab] + [handleReplEval, isRunning, selectedTab] ); - const { persistenceUser, persistenceFile } = props; // Compute this here to avoid re-rendering the button every keystroke const persistenceIsDirty = persistenceFile && (!persistenceFile.lastSaved || persistenceFile.lastSaved < lastEdit); - const persistenceButtons = React.useMemo(() => { + const persistenceButtons = useMemo(() => { return ( = ({ ); }, [isFolderModeEnabled, persistenceFile, persistenceUser, persistenceIsDirty, dispatch]); - const githubOctokitObject = useTypedSelector(store => store.session.githubOctokitObject); - const githubSaveInfo = props.githubSaveInfo; const githubPersistenceIsDirty = githubSaveInfo && (!githubSaveInfo.lastSaved || githubSaveInfo.lastSaved < lastEdit); - const githubButtons = React.useMemo(() => { + const githubButtons = useMemo(() => { return ( = ({ isFolderModeEnabled ]); - const executionTime = React.useMemo( + const executionTime = useMemo( () => ( ), - [props.execTime, props.handleChangeExecTime] + [execTime, handleChangeExecTime] ); - const stepperStepLimit = React.useMemo( + const stepperStepLimit = useMemo( () => ( dispatch(changeStepLimit(limit, workspaceLocation))} handleOnBlurAutoScale={limit => { limit % 2 === 0 @@ -603,29 +618,29 @@ const SicpWorkspace: React.FC = ({ key="step_limit" /> ), - [dispatch, props.stepLimit, workspaceLocation] + [dispatch, stepLimit, workspaceLocation] ); - const shareButton = React.useMemo(() => { - const queryString = Links.playground + '#' + props.initialEditorValueHash; + const shareButton = useMemo(() => { + const qs = Links.playground + '#' + props.initialEditorValueHash; return ( dispatch(generateLzString())} handleShortenURL={s => dispatch(shortenURL(s))} handleUpdateShortURL={s => dispatch(updateShortURL(s))} - queryString={queryString} - shortURL={props.shortURL} + queryString={qs} + shortURL={shortURL} isSicp key="share" /> ); - }, [dispatch, props.initialEditorValueHash, props.shortURL]); + }, [dispatch, props.initialEditorValueHash, shortURL]); - const toggleFolderModeButton = React.useMemo(() => { + const toggleFolderModeButton = useMemo(() => { return ( dispatch(toggleFolderMode(workspaceLocation))} key="folder" @@ -636,41 +651,36 @@ const SicpWorkspace: React.FC = ({ githubSaveInfo.repoName, isFolderModeEnabled, persistenceFile, - props.editorSessionId, + editorSessionId, workspaceLocation ]); - React.useEffect(() => { + useEffect(() => { // TODO: To migrate the state logic away from playgroundSourceChapter // and playgroundSourceVariant into the language config instead - const languageConfigToSet = getLanguageConfig( - props.playgroundSourceChapter, - props.playgroundSourceVariant - ); + const languageConfigToSet = getLanguageConfig(playgroundSourceChapter, playgroundSourceVariant); // Hardcoded for Playground only for now, while we await workspace refactoring // to decouple the SicpWorkspace from the Playground. dispatch(playgroundConfigLanguage(languageConfigToSet)); - }, [dispatch, props.playgroundSourceChapter, props.playgroundSourceVariant]); + }, [dispatch, playgroundSourceChapter, playgroundSourceVariant]); const shouldShowDataVisualizer = languageConfig.supports.dataVisualizer; const shouldShowEnvVisualizer = languageConfig.supports.envVisualizer; const shouldShowSubstVisualizer = languageConfig.supports.substVisualizer; - const playgroundIntroductionTab: SideContentTab = React.useMemo( + const playgroundIntroductionTab: SideContentTab = useMemo( () => makeIntroductionTabFrom(generateLanguageIntroduction(languageConfig)), [languageConfig] ); - - const tabs = React.useMemo(() => { + const tabs = useMemo(() => { const tabs: SideContentTab[] = [playgroundIntroductionTab]; const currentLang = languageConfig.chapter; - if (currentLang === Chapter.HTML) { // For HTML Chapter, HTML Display tab is added only after code is run - if (props.output.length > 0 && props.output[0].type === 'result') { + if (output.length > 0 && output[0].type === 'result') { tabs.push( - makeHtmlDisplayTabFrom(props.output[0] as ResultOutput, errorMsg => + makeHtmlDisplayTabFrom(output[0] as ResultOutput, errorMsg => dispatch(addHtmlConsoleError(errorMsg, workspaceLocation)) ) ); @@ -685,14 +695,14 @@ const SicpWorkspace: React.FC = ({ tabs.push(makeEnvVisualizerTabFrom(workspaceLocation)); } if (shouldShowSubstVisualizer) { - tabs.push(makeSubstVisualizerTabFrom(props.output)); + tabs.push(makeSubstVisualizerTabFrom(output)); } return tabs; }, [ playgroundIntroductionTab, languageConfig.chapter, - props.output, + output, dispatch, workspaceLocation, shouldShowDataVisualizer, @@ -703,7 +713,7 @@ const SicpWorkspace: React.FC = ({ // Remove Intro tab for mobile const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id))); - const onLoadMethod = React.useCallback( + const onLoadMethod = useCallback( (editor: Ace.Editor) => { const addFold = () => { editor.getSession().addFold(' ', new Range(1, 0, props.prependLength!, 0)); @@ -715,7 +725,7 @@ const SicpWorkspace: React.FC = ({ [props.prependLength] ); - const onChangeMethod = React.useCallback( + const onChangeMethod = useCallback( (newCode: string, delta: CodeDelta) => { const input: Input = { time: Date.now(), @@ -730,7 +740,7 @@ const SicpWorkspace: React.FC = ({ [pushLog, dispatch, workspaceLocation] ); - const onCursorChangeMethod = React.useCallback( + const onCursorChangeMethod = useCallback( (selection: any) => { const input: Input = { time: Date.now(), @@ -743,7 +753,7 @@ const SicpWorkspace: React.FC = ({ [pushLog] ); - const onSelectionChangeMethod = React.useCallback( + const onSelectionChangeMethod = useCallback( (selection: any) => { const range: SelectionRange = selection.getRange(); const isBackwards: boolean = selection.isBackwards(); @@ -760,72 +770,81 @@ const SicpWorkspace: React.FC = ({ [pushLog] ); - const handleEditorUpdateBreakpoints = React.useCallback( + const handleEditorUpdateBreakpoints = useCallback( (editorTabIndex: number, breakpoints: string[]) => { // get rid of holes in array const numberOfBreakpoints = breakpoints.filter(arrayItem => !!arrayItem).length; if (numberOfBreakpoints > 0) { setHasBreakpoints(true); - if (propsRef.current.playgroundSourceChapter <= 2) { + if (playgroundSourceChapter <= 2) { /** * There are breakpoints set on Source Chapter 2, so we set the * Redux state for the editor to evaluate to the substituter */ - propsRef.current.handleUsingSubst(true); + handleUsingSubst(true); } } if (numberOfBreakpoints === 0) { setHasBreakpoints(false); if (selectedTab !== SideContentType.substVisualizer) { - propsRef.current.handleReplOutputClear(); - propsRef.current.handleUsingSubst(false); + handleReplOutputClear(); + handleUsingSubst(false); } } - propsRef.current.handleEditorUpdateBreakpoints(editorTabIndex, breakpoints); + handleSetEditorBreakpoints(editorTabIndex, breakpoints); dispatch(toggleUpdateEnv(true, workspaceLocation)); }, - [selectedTab, dispatch, workspaceLocation] + [ + selectedTab, + dispatch, + workspaceLocation, + handleSetEditorBreakpoints, + handleReplOutputClear, + handleUsingSubst, + playgroundSourceChapter + ] ); const replDisabled = !languageConfig.supports.repl; - const setActiveEditorTabIndex = React.useCallback( + const setActiveEditorTabIndex = useCallback( (activeEditorTabIndex: number | null) => dispatch(updateActiveEditorTabIndex(workspaceLocation, activeEditorTabIndex)), [dispatch, workspaceLocation] ); - const removeEditorTabByIndex = React.useCallback( + const removeEditorTabByIndex = useCallback( (editorTabIndex: number) => dispatch(removeEditorTab(workspaceLocation, editorTabIndex)), [dispatch, workspaceLocation] ); const editorContainerProps: NormalEditorContainerProps = { - ..._.pick(props, 'editorSessionId', 'isEditorAutorun'), + editorSessionId, + isEditorAutorun, editorVariant: 'normal', baseFilePath: WORKSPACE_BASE_PATHS[workspaceLocation], isFolderModeEnabled, activeEditorTabIndex, setActiveEditorTabIndex, removeEditorTabByIndex, - editorTabs: props.editorTabs.map(convertEditorTabStateToProps), - handleDeclarationNavigate: React.useCallback( + editorTabs: editorTabs.map(convertEditorTabStateToProps), + handleDeclarationNavigate: useCallback( (cursorPosition: Position) => dispatch(navigateToDeclaration(workspaceLocation, cursorPosition)), [dispatch, workspaceLocation] ), handleEditorEval: memoizedHandlers.handleEditorEval, - handlePromptAutocomplete: React.useCallback( + handlePromptAutocomplete: useCallback( (row: number, col: number, callback: any) => dispatch(promptAutocomplete(workspaceLocation, row, col, callback)), [dispatch, workspaceLocation] ), - handleSendReplInputToOutput: React.useCallback( + handleSendReplInputToOutput: useCallback( (code: string) => dispatch(sendReplInputToOutput(code, workspaceLocation)), [dispatch, workspaceLocation] ), - handleSetSharedbConnected: React.useCallback( + handleSetSharedbConnected: useCallback( (connected: boolean) => dispatch(setSharedbConnected(workspaceLocation, connected)), [dispatch, workspaceLocation] ), @@ -841,16 +860,19 @@ const SicpWorkspace: React.FC = ({ }; const replProps = { - ..._.pick(props, 'output', 'replValue', 'handleReplEval', 'usingSubst'), - handleBrowseHistoryDown: React.useCallback( + output, + replValue, + handleReplEval, + usingSubst, + handleBrowseHistoryDown: useCallback( () => dispatch(browseReplHistoryDown(workspaceLocation)), [dispatch, workspaceLocation] ), - handleBrowseHistoryUp: React.useCallback( + handleBrowseHistoryUp: useCallback( () => dispatch(browseReplHistoryUp(workspaceLocation)), [dispatch, workspaceLocation] ), - handleReplValueChange: React.useCallback( + handleReplValueChange: useCallback( (newValue: string) => dispatch(updateReplValue(newValue, workspaceLocation)), [dispatch, workspaceLocation] ), @@ -865,7 +887,7 @@ const SicpWorkspace: React.FC = ({ disableScrolling: true }; - const sideBarProps: { tabs: SideBarTab[] } = React.useMemo(() => { + const sideBarProps: { tabs: SideBarTab[] } = useMemo(() => { // The sidebar is rendered if and only if there is at least one tab present. // Because whether the sidebar is rendered or not affects the sidebar resizing // logic, we cannot defer the decision on which sidebar tabs should be rendered @@ -898,25 +920,25 @@ const SicpWorkspace: React.FC = ({ editorButtons: [ autorunButtons, languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, - chapterSelect, + chapterSelectButton, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons, !isSourceLanguage(languageConfig.chapter) ? null - : props.usingSubst + : usingSubst ? stepperStepLimit : executionTime ] }, editorContainerProps: editorContainerProps, - handleSideContentHeightChange: React.useCallback( + handleSideContentHeightChange: useCallback( change => dispatch(changeSideContentHeight(change, workspaceLocation)), [dispatch, workspaceLocation] ), replProps: replProps, sideBarProps: sideBarProps, - sideContentHeight: props.sideContentHeight, + sideContentHeight: sideContentHeight, sideContentProps: { selectedTabId: selectedTab, onChange: onChangeTabs, @@ -925,7 +947,7 @@ const SicpWorkspace: React.FC = ({ afterDynamicTabs: [] }, workspaceLocation: 'sicp', - sideContentHeight: props.sideContentHeight + sideContentHeight: sideContentHeight }, sideContentIsResizeable: selectedTab !== SideContentType.substVisualizer }; @@ -938,7 +960,7 @@ const SicpWorkspace: React.FC = ({ mobileControlBarProps: { editorButtons: [ autorunButtons, - chapterSelect, + chapterSelectButton, languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, @@ -970,4 +992,9 @@ const SicpWorkspace: React.FC = ({ ); }; +// react-router lazy loading +// https://reactrouter.com/en/main/route/lazy +export const Component = SicpWorkspace; +Component.displayName = 'SicpWorkspace'; + export default SicpWorkspace; From bb6ee59059c8bdc3879dc087c32ecb01fab408fd Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 15 Jun 2023 02:41:02 +0800 Subject: [PATCH 08/12] Restore changes to Playground post-merge --- src/pages/playground/Playground.tsx | 45 ++++++++--------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index e612f77a3f..9daa4391e8 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -1,6 +1,5 @@ import { Classes } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; -import { Ace, Range } from 'ace-builds'; import { FSModule } from 'browserfs/dist/node/core/FS'; import classNames from 'classnames'; import { Chapter, Variant } from 'js-slang/dist/types'; @@ -115,7 +114,6 @@ import MobileWorkspace, { } from '../../commons/mobileWorkspace/MobileWorkspace'; import { SideBarTab } from '../../commons/sideBar/SideBar'; import { SideContentTab, SideContentType } from '../../commons/sideContent/SideContentTypes'; -import { Links } from '../../commons/utils/Constants'; import { generateLanguageIntroduction } from '../../commons/utils/IntroductionHelper'; import { convertParamToBoolean, convertParamToInt } from '../../commons/utils/ParamParseHelper'; import { IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper'; @@ -231,11 +229,8 @@ export async function handleHash( } } -// FIXME: Remove this after merging is complete -const isSicpEditor = false; - const Playground: React.FC = props => { - const workspaceLocation: WorkspaceLocation = isSicpEditor ? 'sicp' : 'playground'; + const workspaceLocation: WorkspaceLocation = 'playground'; const { isMobileBreakpoint } = useResponsive(); const [deviceSecret, setDeviceSecret] = useState(); @@ -328,8 +323,7 @@ const Playground: React.FC = props => { [deviceSecret] ); - const usingRemoteExecution = - useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; + const usingRemoteExecution = useTypedSelector(state => !!state.session.remoteExecutionSession); // this is still used by remote execution (EV3) // specifically, for the editor Ctrl+B to work const externalLibraryName = useTypedSelector( @@ -348,7 +342,7 @@ const Playground: React.FC = props => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [editorSessionId]); - const hash = isSicpEditor ? props.initialEditorValueHash : location.hash; + const hash = location.hash; useEffect(() => { if (!hash) { @@ -692,7 +686,7 @@ const Playground: React.FC = props => { ); const shareButton = useMemo(() => { - const qs = isSicpEditor ? Links.playground + '#' + props.initialEditorValueHash : queryString; + const qs = queryString; return ( dispatch(generateLzString())} @@ -700,11 +694,11 @@ const Playground: React.FC = props => { handleUpdateShortURL={s => dispatch(updateShortURL(s))} queryString={qs} shortURL={shortURL} - isSicp={isSicpEditor} + isSicp={false} key="share" /> ); - }, [dispatch, props.initialEditorValueHash, queryString, shortURL]); + }, [dispatch, queryString, shortURL]); const toggleFolderModeButton = useMemo(() => { return ( @@ -771,9 +765,7 @@ const Playground: React.FC = props => { } } - if (!isSicpEditor) { - tabs.push(remoteExecutionTab); - } + tabs.push(remoteExecutionTab); return tabs; }, [ @@ -792,18 +784,6 @@ const Playground: React.FC = props => { // Remove Intro and Remote Execution tabs for mobile const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id))); - const onLoadMethod = useCallback( - (editor: Ace.Editor) => { - const addFold = () => { - editor.getSession().addFold(' ', new Range(1, 0, props.prependLength!, 0)); - editor.renderer.off('afterRender', addFold); - }; - - editor.renderer.on('afterRender', addFold); - }, - [props.prependLength] - ); - const onChangeMethod = useCallback( (newCode: string, delta: CodeDelta) => { const input: Input = { @@ -930,7 +910,6 @@ const Playground: React.FC = props => { onChange: onChangeMethod, onCursorChange: onCursorChangeMethod, onSelectionChange: onSelectionChangeMethod, - onLoad: isSicpEditor && props.prependLength ? onLoadMethod : undefined, sourceChapter: languageConfig.chapter, externalLibraryName, sourceVariant: languageConfig.variant, @@ -963,7 +942,7 @@ const Playground: React.FC = props => { selectedTab === SideContentType.envVisualizer, inputHidden: replDisabled, replButtons: [replDisabled ? null : evalButton, clearButton], - disableScrolling: isSicpEditor + disableScrolling: false }; const sideBarProps: { tabs: SideBarTab[] } = useMemo(() => { @@ -1000,7 +979,7 @@ const Playground: React.FC = props => { autorunButtons, languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, chapterSelectButton, - isSicpEditor ? null : sessionButtons, + sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons, @@ -1026,7 +1005,7 @@ const Playground: React.FC = props => { beforeDynamicTabs: tabs, afterDynamicTabs: [] }, - workspaceLocation: isSicpEditor ? 'sicp' : 'playground', + workspaceLocation: 'playground', sideContentHeight: sideContentHeight }, sideContentIsResizeable: selectedTab !== SideContentType.substVisualizer @@ -1042,7 +1021,7 @@ const Playground: React.FC = props => { autorunButtons, chapterSelectButton, languageConfig.chapter === Chapter.FULL_JS ? null : shareButton, - isSicpEditor ? null : sessionButtons, + sessionButtons, languageConfig.supports.multiFile ? toggleFolderModeButton : null, persistenceButtons, githubButtons @@ -1054,7 +1033,7 @@ const Playground: React.FC = props => { beforeDynamicTabs: mobileTabs, afterDynamicTabs: [] }, - workspaceLocation: isSicpEditor ? 'sicp' : 'playground' + workspaceLocation: 'playground' } }; From 6511478f6029a865e90283ffb18674503209c3ee Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Thu, 15 Jun 2023 02:43:41 +0800 Subject: [PATCH 09/12] Update snapshots post-merge --- .../__snapshots__/Playground.tsx.snap | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap b/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap index 490a24ecf1..1b080e28a0 100644 --- a/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap +++ b/src/pages/playground/__tests__/__snapshots__/Playground.tsx.snap @@ -67,7 +67,7 @@ exports[`Playground tests Playground renders correctly 1`] = ` - + @@ -408,10 +408,10 @@ exports[`Playground tests Playground renders correctly 1`] = `
- +
- - + +
@@ -807,13 +807,13 @@ exports[`Playground tests Playground renders correctly 1`] = `
-