Skip to content

Commit f9a23f9

Browse files
committed
feat: add vscode slice for message passing with extension
1 parent d155aec commit f9a23f9

File tree

13 files changed

+197
-34
lines changed

13 files changed

+197
-34
lines changed

src/commons/application/Application.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import React from 'react';
22
import { useDispatch } from 'react-redux';
33
import { Outlet } from 'react-router-dom';
4+
import Messages, { MessageType, sendToWebview } from 'src/features/vscode/messages';
45

56
import NavigationBar from '../navigationBar/NavigationBar';
67
import Constants from '../utils/Constants';
78
import { useLocalStorageState, useSession } from '../utils/Hooks';
9+
import WorkspaceActions from '../workspace/WorkspaceActions';
810
import { defaultWorkspaceSettings, WorkspaceSettingsContext } from '../WorkspaceSettingsContext';
911
import SessionActions from './actions/SessionActions';
12+
import VscodeActions from './actions/VscodeActions';
1013

1114
const Application: React.FC = () => {
1215
const dispatch = useDispatch();
@@ -70,6 +73,47 @@ const Application: React.FC = () => {
7073
};
7174
}, [isPWA, isMobile]);
7275

76+
// Effect to fetch the latest user info and course configurations from the backend on refresh,
77+
// if the user was previously logged in
78+
React.useEffect(() => {
79+
// Polyfill confirm() to instead show as VSCode notification
80+
window.confirm = () => {
81+
console.log('You gotta confirm!');
82+
return true;
83+
};
84+
85+
const message = Messages.WebviewStarted();
86+
sendToWebview(message);
87+
88+
window.addEventListener('message', event => {
89+
const message: MessageType = event.data;
90+
// Only accept messages from the vscode webview
91+
if (!event.origin.startsWith('vscode-webview://')) {
92+
return;
93+
}
94+
// console.log(`FRONTEND: Message from ${event.origin}: ${JSON.stringify(message)}`);
95+
switch (message.type) {
96+
case 'WebviewStarted':
97+
console.log('Received WebviewStarted message, will set vsc');
98+
dispatch(VscodeActions.setVscode());
99+
break;
100+
case 'Text':
101+
const code = message.code;
102+
console.log(`FRONTEND: TextMessage: ${code}`);
103+
// TODO: Don't change ace editor directly
104+
// const elements = document.getElementsByClassName('react-ace');
105+
// if (elements.length === 0) {
106+
// return;
107+
// }
108+
// // @ts-expect-error: ace is not available at compile time
109+
// const editor = ace.edit(elements[0]);
110+
// editor.setValue(code);
111+
dispatch(WorkspaceActions.updateEditorValue('assessment', 0, code));
112+
break;
113+
}
114+
});
115+
}, []);
116+
73117
return (
74118
<WorkspaceSettingsContext.Provider value={[workspaceSettings, setWorkspaceSettings]}>
75119
<div className="Application">

src/commons/application/ApplicationTypes.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { RouterState } from './types/CommonsTypes';
2121
import { ExternalLibraryName } from './types/ExternalTypes';
2222
import { SessionState } from './types/SessionTypes';
23+
import { VscodeState as VscodeState } from './types/VscodeTypes';
2324

2425
export type OverallState = {
2526
readonly router: RouterState;
@@ -31,6 +32,7 @@ export type OverallState = {
3132
readonly dashboard: DashboardState;
3233
readonly fileSystem: FileSystemState;
3334
readonly sideContent: SideContentManagerState;
35+
readonly vscode: VscodeState;
3436
};
3537

3638
export type Story = {
@@ -604,6 +606,10 @@ export const defaultSideContentManager: SideContentManagerState = {
604606
stories: {}
605607
};
606608

609+
export const defaultVscode: VscodeState = {
610+
isVscode: false
611+
};
612+
607613
export const defaultState: OverallState = {
608614
router: defaultRouter,
609615
achievement: defaultAchievement,
@@ -613,5 +619,6 @@ export const defaultState: OverallState = {
613619
stories: defaultStories,
614620
workspaces: defaultWorkspaceManager,
615621
fileSystem: defaultFileSystem,
616-
sideContent: defaultSideContentManager
622+
sideContent: defaultSideContentManager,
623+
vscode: defaultVscode
617624
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createActions } from 'src/commons/redux/utils';
2+
3+
const VscodeActions = createActions('vscode', {
4+
setVscode: () => ({})
5+
});
6+
7+
// For compatibility with existing code (actions helper)
8+
export default {
9+
...VscodeActions
10+
};

src/commons/application/reducers/RootReducer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { WorkspaceReducer as workspaces } from '../../workspace/WorkspaceReducer
1111
import { OverallState } from '../ApplicationTypes';
1212
import { RouterReducer as router } from './CommonsReducer';
1313
import { SessionsReducer as session } from './SessionsReducer';
14+
import { VscodeReducer as vscode } from './VscodeReducer';
1415

1516
const rootReducer: Reducer<OverallState, SourceActionType> = combineReducers({
1617
router,
@@ -21,7 +22,8 @@ const rootReducer: Reducer<OverallState, SourceActionType> = combineReducers({
2122
stories,
2223
workspaces,
2324
fileSystem,
24-
sideContent
25+
sideContent,
26+
vscode
2527
});
2628

2729
export default rootReducer;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createReducer, Reducer } from '@reduxjs/toolkit';
2+
3+
import { SourceActionType } from '../../utils/ActionsHelper';
4+
import VscodeActions from '../actions/VscodeActions';
5+
import { defaultVscode } from '../ApplicationTypes';
6+
import { VscodeState } from '../types/VscodeTypes';
7+
8+
export const VscodeReducer: Reducer<VscodeState, SourceActionType> = (
9+
state = defaultVscode,
10+
action
11+
) => {
12+
state = newVscodeReducer(state, action);
13+
return state;
14+
};
15+
16+
const newVscodeReducer = createReducer(defaultVscode, builder => {
17+
builder.addCase(VscodeActions.setVscode, state => {
18+
return { ...state, ...{ isVscode: true } };
19+
});
20+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type VscodeState = {
2+
isVscode: boolean;
3+
};

src/commons/assessmentWorkspace/AssessmentWorkspace.tsx

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useDispatch } from 'react-redux';
1919
import { useNavigate } from 'react-router';
2020
import { showSimpleConfirmDialog } from 'src/commons/utils/DialogHelper';
2121
import { onClickProgress } from 'src/features/assessments/AssessmentUtils';
22+
import Messages, { sendToWebview } from 'src/features/vscode/messages';
2223
import { mobileOnlyTabIds } from 'src/pages/playground/PlaygroundTabs';
2324

2425
import { initSession, log } from '../../features/eventLogging';
@@ -184,11 +185,11 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
184185
};
185186
}, [dispatch]);
186187

187-
useEffect(() => {
188-
// TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
189-
handleEditorValueChange(0, '');
190-
// eslint-disable-next-line react-hooks/exhaustive-deps
191-
}, []);
188+
// useEffect(() => {
189+
// // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
190+
// handleEditorValueChange(0, '');
191+
// // eslint-disable-next-line react-hooks/exhaustive-deps
192+
// }, []);
192193

193194
useEffect(() => {
194195
if (assessmentOverview && assessmentOverview.maxTeamSize > 1) {
@@ -220,27 +221,27 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
220221
if (!assessment) {
221222
return;
222223
}
223-
// ------------- PLEASE NOTE, EVERYTHING BELOW THIS SEEMS TO BE UNUSED -------------
224-
// checkWorkspaceReset does exactly the same thing.
225-
let questionId = props.questionId;
226-
if (props.questionId >= assessment.questions.length) {
227-
questionId = assessment.questions.length - 1;
228-
}
229-
230-
const question = assessment.questions[questionId];
231-
232-
let answer = '';
233-
if (question.type === QuestionTypes.programming) {
234-
if (question.answer) {
235-
answer = (question as IProgrammingQuestion).answer as string;
236-
} else {
237-
answer = (question as IProgrammingQuestion).solutionTemplate;
238-
}
239-
}
240-
241-
// TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
242-
handleEditorValueChange(0, answer);
243-
// eslint-disable-next-line react-hooks/exhaustive-deps
224+
// // ------------- PLEASE NOTE, EVERYTHING BELOW THIS SEEMS TO BE UNUSED -------------
225+
// // checkWorkspaceReset does exactly the same thing.
226+
// let questionId = props.questionId;
227+
// if (props.questionId >= assessment.questions.length) {
228+
// questionId = assessment.questions.length - 1;
229+
// }
230+
231+
// const question = assessment.questions[questionId];
232+
233+
// let answer = '';
234+
// if (question.type === QuestionTypes.programming) {
235+
// if (question.answer) {
236+
// answer = (question as IProgrammingQuestion).answer as string;
237+
// } else {
238+
// answer = (question as IProgrammingQuestion).solutionTemplate;
239+
// }
240+
// }
241+
242+
// // TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
243+
// handleEditorValueChange(0, answer);
244+
// // eslint-disable-next-line react-hooks/exhaustive-deps
244245
}, []);
245246

246247
/**
@@ -415,9 +416,12 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
415416
);
416417
handleClearContext(question.library, true);
417418
handleUpdateHasUnsavedChanges(false);
419+
sendToWebview(Messages.NewEditor(`assessment${assessment.id}`, props.questionId, ''));
418420
if (options.editorValue) {
419421
// TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
420422
handleEditorValueChange(0, options.editorValue);
423+
} else {
424+
handleEditorValueChange(0, '');
421425
}
422426
};
423427

src/commons/mocks/StoreMocks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
defaultSession,
1212
defaultSideContentManager,
1313
defaultStories,
14+
defaultVscode,
1415
defaultWorkspaceManager,
1516
OverallState
1617
} from '../application/ApplicationTypes';
@@ -30,7 +31,8 @@ export function mockInitialStore(
3031
session: defaultSession,
3132
stories: defaultStories,
3233
fileSystem: defaultFileSystem,
33-
sideContent: defaultSideContentManager
34+
sideContent: defaultSideContentManager,
35+
vscode: defaultVscode
3436
};
3537

3638
const lodashMergeCustomizer = (objValue: any, srcValue: any) => {

src/commons/utils/ActionsHelper.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import SourcecastActions from '../../features/sourceRecorder/sourcecast/Sourceca
1717
import SourceRecorderActions from '../../features/sourceRecorder/SourceRecorderActions';
1818
import SourcereelActions from '../../features/sourceRecorder/sourcereel/SourcereelActions';
1919
import StoriesActions from '../../features/stories/StoriesActions';
20+
import VscodeActions from '../application/actions/VscodeActions';
2021
import { ActionType } from './TypeHelper';
2122

2223
export const actions = {
@@ -38,7 +39,8 @@ export const actions = {
3839
...RemoteExecutionActions,
3940
...FileSystemActions,
4041
...StoriesActions,
41-
...SideContentActions
42+
...SideContentActions,
43+
...VscodeActions
4244
};
4345

4446
export type SourceActionType = ActionType<typeof actions>;

src/commons/workspace/Workspace.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Prompt } from '../ReactRouterPrompt';
1212
import Repl, { ReplProps } from '../repl/Repl';
1313
import SideBar, { SideBarTab } from '../sideBar/SideBar';
1414
import SideContent, { SideContentProps } from '../sideContent/SideContent';
15-
import { useDimensions } from '../utils/Hooks';
15+
import { useDimensions, useTypedSelector } from '../utils/Hooks';
1616

1717
export type WorkspaceProps = DispatchProps & StateProps;
1818

@@ -44,6 +44,7 @@ const Workspace: React.FC<WorkspaceProps> = props => {
4444
const [contentContainerWidth] = useDimensions(contentContainerDiv);
4545
const [expandedSideBarWidth, setExpandedSideBarWidth] = useState(200);
4646
const [isSideBarExpanded, setIsSideBarExpanded] = useState(true);
47+
const isVscode = useTypedSelector(state => state.vscode.isVscode);
4748

4849
const sideBarCollapsedWidth = 40;
4950

@@ -222,7 +223,12 @@ const Workspace: React.FC<WorkspaceProps> = props => {
222223
</Resizable>
223224
<div className="row content-parent" ref={contentContainerDiv}>
224225
<div className="editor-divider" ref={editorDividerDiv} />
225-
<Resizable {...editorResizableProps()}>{createWorkspaceInput(props)}</Resizable>
226+
{isVscode ? (
227+
<div style={{ width: '0px' }}>{createWorkspaceInput(props)}</div>
228+
) : (
229+
<Resizable {...editorResizableProps()}>{createWorkspaceInput(props)}</Resizable>
230+
)}
231+
<div style={{ width: '0px' }}>{createWorkspaceInput(props)}</div>
226232
<div className="right-parent" ref={setFullscreenRefs}>
227233
<Tooltip
228234
className="fullscreen-button"

src/commons/workspace/WorkspaceActions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ const newActions = createActions('workspace', {
113113
updateEditorValue: (
114114
workspaceLocation: WorkspaceLocation,
115115
editorTabIndex: number,
116-
newEditorValue: string
117-
) => ({ workspaceLocation, editorTabIndex, newEditorValue }),
116+
newEditorValue: string,
117+
isFromVscode: boolean = false
118+
) => ({ workspaceLocation, editorTabIndex, newEditorValue, isFromVscode }),
118119
setEditorBreakpoint: (
119120
workspaceLocation: WorkspaceLocation,
120121
editorTabIndex: number,

src/commons/workspace/reducers/editorReducer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
2+
import Messages, { sendToWebview } from 'src/features/vscode/messages';
23

34
import WorkspaceActions from '../WorkspaceActions';
45
import { getWorkspaceLocation } from '../WorkspaceReducer';
@@ -52,6 +53,9 @@ export const handleEditorActions = (builder: ActionReducerMapBuilder<WorkspaceMa
5253
}
5354

5455
state[workspaceLocation].editorTabs[editorTabIndex].value = newEditorValue;
56+
if (!action.payload.isFromVscode) {
57+
sendToWebview(Messages.Text(newEditorValue));
58+
}
5559
})
5660
.addCase(WorkspaceActions.setEditorBreakpoint, (state, action) => {
5761
const workspaceLocation = getWorkspaceLocation(action);

src/features/vscode/messages.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// This file also needs to be copied to source-academy/frontend
2+
type BaseMessage<T extends string, P extends object> = {
3+
type: T;
4+
} & P;
5+
6+
function createMessages<T extends Record<string, (...args: any[]) => object>>(
7+
creators: T
8+
): {
9+
[K in Extract<keyof T, string>]: (...args: Parameters<T[K]>) => BaseMessage<K, ReturnType<T[K]>>;
10+
} {
11+
return Object.fromEntries(
12+
Object.entries(creators).map(([key, creator]) => [
13+
key,
14+
(...args: any[]) => ({
15+
type: key,
16+
...creator(...args)
17+
})
18+
])
19+
) as any;
20+
}
21+
22+
const Messages = createMessages({
23+
WebviewStarted: () => ({}),
24+
IsVsc: () => ({}),
25+
NewEditor: (assessmentName: string, questionId: number, code: string) => ({
26+
assessmentName,
27+
questionId,
28+
code
29+
}),
30+
Text: (code: string) => ({ code })
31+
});
32+
33+
export default Messages;
34+
35+
// Define MessageTypes to map each key in Messages to its specific message type
36+
export type MessageTypes = {
37+
[K in keyof typeof Messages]: ReturnType<(typeof Messages)[K]>;
38+
};
39+
40+
// Define MessageType as a union of all message types
41+
export type MessageType = MessageTypes[keyof MessageTypes];
42+
43+
export const FRONTEND_ELEMENT_ID = 'frontend';
44+
45+
export function sendToWebview(message: MessageType) {
46+
window.parent.postMessage(message, '*');
47+
}
48+
export function sendToFrontend(document: Document, message: MessageType) {
49+
const iframe: HTMLIFrameElement = document.getElementById(
50+
FRONTEND_ELEMENT_ID
51+
) as HTMLIFrameElement;
52+
const contentWindow = iframe.contentWindow;
53+
if (!contentWindow) {
54+
return;
55+
}
56+
// TODO: Don't hardcode this!
57+
contentWindow.postMessage(message, 'http://localhost:8000');
58+
}

0 commit comments

Comments
 (0)