Skip to content

Commit 5eae080

Browse files
heyzecRichDom2185
andauthored
feat: add vscode slice for message passing with extension (#3080)
* feat: add vscode slice for message passing with extension * fix tests * Update src/commons/application/actions/VscodeActions.ts Co-authored-by: Richard Dominick <[email protected]> * address comments * fix logical error * refactor: improve typing of Messages util * Fetch user and course after login * Remove unused code * Polyfill window.confirm conditionally * fix linting errors --------- Co-authored-by: Richard Dominick <[email protected]>
1 parent 407cebd commit 5eae080

File tree

14 files changed

+224
-34
lines changed

14 files changed

+224
-34
lines changed

src/commons/application/Application.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import React from 'react';
22
import { useDispatch } from 'react-redux';
33
import { Outlet } from 'react-router-dom';
4+
import Messages, {
5+
MessageType,
6+
MessageTypeNames,
7+
sendToWebview
8+
} from 'src/features/vscode/messages';
49

510
import NavigationBar from '../navigationBar/NavigationBar';
611
import Constants from '../utils/Constants';
712
import { useLocalStorageState, useSession } from '../utils/Hooks';
13+
import WorkspaceActions from '../workspace/WorkspaceActions';
814
import { defaultWorkspaceSettings, WorkspaceSettingsContext } from '../WorkspaceSettingsContext';
915
import SessionActions from './actions/SessionActions';
16+
import VscodeActions from './actions/VscodeActions';
1017

1118
const Application: React.FC = () => {
1219
const dispatch = useDispatch();
@@ -70,6 +77,62 @@ const Application: React.FC = () => {
7077
};
7178
}, [isPWA, isMobile]);
7279

80+
// Effect to handle messages from VS Code
81+
React.useEffect(() => {
82+
if (!window.confirm) {
83+
// Polyfill confirm() to instead show as VS Code notification
84+
// TODO: Pass text as a new Message to the webview
85+
window.confirm = text => {
86+
console.log(`Confirmation automatically accepted: ${text ?? 'No text provided'}`);
87+
return true;
88+
};
89+
}
90+
91+
const message = Messages.ExtensionPing();
92+
sendToWebview(message);
93+
94+
window.addEventListener('message', event => {
95+
const message: MessageType = event.data;
96+
// Only accept messages from the vscode webview
97+
if (!event.origin.startsWith('vscode-webview://')) {
98+
return;
99+
}
100+
// console.log(`FRONTEND: Message from ${event.origin}: ${JSON.stringify(message)}`);
101+
switch (message.type) {
102+
case MessageTypeNames.ExtensionPong:
103+
console.log('Received WebviewStarted message, will set vsc');
104+
dispatch(VscodeActions.setVscode());
105+
106+
if (message.token) {
107+
const token = JSON.parse(message.token.trim());
108+
console.log(`FRONTEND: WebviewStarted: ${token}`);
109+
dispatch(
110+
SessionActions.setTokens({
111+
accessToken: token.accessToken,
112+
refreshToken: token.refreshToken
113+
})
114+
);
115+
dispatch(SessionActions.fetchUserAndCourse());
116+
}
117+
break;
118+
case MessageTypeNames.Text:
119+
const code = message.code;
120+
console.log(`FRONTEND: TextMessage: ${code}`);
121+
// TODO: Don't change ace editor directly
122+
// const elements = document.getElementsByClassName('react-ace');
123+
// if (elements.length === 0) {
124+
// return;
125+
// }
126+
// // @ts-expect-error: ace is not available at compile time
127+
// const editor = ace.edit(elements[0]);
128+
// editor.setValue(code);
129+
dispatch(WorkspaceActions.updateEditorValue('assessment', 0, code));
130+
break;
131+
}
132+
});
133+
// eslint-disable-next-line react-hooks/exhaustive-deps
134+
}, []);
135+
73136
return (
74137
<WorkspaceSettingsContext.Provider value={[workspaceSettings, setWorkspaceSettings]}>
75138
<div className="Application">

src/commons/application/ApplicationTypes.ts

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

2526
export type OverallState = {
2627
readonly router: RouterState;
@@ -33,6 +34,7 @@ export type OverallState = {
3334
readonly featureFlags: FeatureFlagsState;
3435
readonly fileSystem: FileSystemState;
3536
readonly sideContent: SideContentManagerState;
37+
readonly vscode: VscodeState;
3638
};
3739

3840
export type Story = {
@@ -602,6 +604,10 @@ export const defaultSideContentManager: SideContentManagerState = {
602604
stories: {}
603605
};
604606

607+
export const defaultVscode: VscodeState = {
608+
isVscode: false
609+
};
610+
605611
export const defaultState: OverallState = {
606612
router: defaultRouter,
607613
achievement: defaultAchievement,
@@ -612,5 +618,6 @@ export const defaultState: OverallState = {
612618
workspaces: defaultWorkspaceManager,
613619
featureFlags: defaultFeatureFlags,
614620
fileSystem: defaultFileSystem,
615-
sideContent: defaultSideContentManager
621+
sideContent: defaultSideContentManager,
622+
vscode: defaultVscode
616623
};
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: 0
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
@@ -12,6 +12,7 @@ import { WorkspaceReducer as workspaces } from '../../workspace/WorkspaceReducer
1212
import { OverallState } from '../ApplicationTypes';
1313
import { RouterReducer as router } from './CommonsReducer';
1414
import { SessionsReducer as session } from './SessionsReducer';
15+
import { VscodeReducer as vscode } from './VscodeReducer';
1516

1617
const rootReducer: Reducer<OverallState, SourceActionType> = combineReducers({
1718
router,
@@ -23,7 +24,8 @@ const rootReducer: Reducer<OverallState, SourceActionType> = combineReducers({
2324
workspaces,
2425
featureFlags,
2526
fileSystem,
26-
sideContent
27+
sideContent,
28+
vscode
2729
});
2830

2931
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: 4 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,12 +185,6 @@ 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-
}, []);
192-
193188
useEffect(() => {
194189
if (assessmentOverview && assessmentOverview.maxTeamSize > 1) {
195190
handleTeamOverviewFetch(props.assessmentId);
@@ -220,26 +215,6 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
220215
if (!assessment) {
221216
return;
222217
}
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);
243218
// eslint-disable-next-line react-hooks/exhaustive-deps
244219
}, []);
245220

@@ -415,9 +390,12 @@ const AssessmentWorkspace: React.FC<AssessmentWorkspaceProps> = props => {
415390
);
416391
handleClearContext(question.library, true);
417392
handleUpdateHasUnsavedChanges(false);
393+
sendToWebview(Messages.NewEditor(`assessment${assessment.id}`, props.questionId, ''));
418394
if (options.editorValue) {
419395
// TODO: Hardcoded to make use of the first editor tab. Refactoring is needed for this workspace to enable Folder mode.
420396
handleEditorValueChange(0, options.editorValue);
397+
} else {
398+
handleEditorValueChange(0, '');
421399
}
422400
};
423401

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';
@@ -32,7 +33,8 @@ export function mockInitialStore(
3233
stories: defaultStories,
3334
featureFlags: defaultFeatureFlags,
3435
fileSystem: defaultFileSystem,
35-
sideContent: defaultSideContentManager
36+
sideContent: defaultSideContentManager,
37+
vscode: defaultVscode
3638
};
3739

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

src/commons/utils/ActionsHelper.ts

Lines changed: 3 additions & 0 deletions
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 { FeatureFlagsActions } from '../featureFlags';
2122
import { ActionType } from './TypeHelper';
2223

@@ -40,6 +41,8 @@ export const actions = {
4041
...FileSystemActions,
4142
...StoriesActions,
4243
...SideContentActions,
44+
...VscodeActions,
45+
...SideContentActions,
4346
...FeatureFlagsActions
4447
};
4548

src/commons/workspace/Workspace.tsx

Lines changed: 5 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,9 @@ 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+
<Resizable {...editorResizableProps()}>{createWorkspaceInput(props)}</Resizable>
228+
)}
226229
<div className="right-parent" ref={setFullscreenRefs}>
227230
<Tooltip
228231
className="fullscreen-button"

src/commons/workspace/WorkspaceActions.ts

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

src/commons/workspace/__tests__/WorkspaceActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ test('updateEditorValue generates correct action object', () => {
258258
payload: {
259259
workspaceLocation: assessmentWorkspace,
260260
editorTabIndex,
261-
newEditorValue
261+
newEditorValue,
262+
isFromVscode: false
262263
}
263264
});
264265
});

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);

0 commit comments

Comments
 (0)