forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathstore.ts
More file actions
457 lines (412 loc) · 19.4 KB
/
store.ts
File metadata and controls
457 lines (412 loc) · 19.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
'use strict';
import * as fastDeepEqual from 'fast-deep-equal';
import * as path from 'path';
import * as Redux from 'redux';
import { createLogger } from 'redux-logger';
import { PYTHON_LANGUAGE } from '../../../client/common/constants';
import { EXTENSION_ROOT_DIR } from '../../../client/constants';
import { Identifiers } from '../../../client/datascience/constants';
import { InteractiveWindowMessages } from '../../../client/datascience/interactive-common/interactiveWindowTypes';
import { MessageType } from '../../../client/datascience/interactive-common/synchronization';
import { BaseReduxActionPayload } from '../../../client/datascience/interactive-common/types';
import { CssMessages } from '../../../client/datascience/messages';
import { CellState } from '../../../client/datascience/types';
import {
activeDebugState,
DebugState,
getSelectedAndFocusedInfo,
ICellViewModel,
IMainState,
ServerStatus
} from '../../interactive-common/mainState';
import { getLocString } from '../../react-common/locReactSide';
import { PostOffice } from '../../react-common/postOffice';
import { combineReducers, createQueueableActionMiddleware, QueuableAction } from '../../react-common/reduxUtils';
import { computeEditorOptions, getDefaultSettings } from '../../react-common/settingsReactSide';
import { createEditableCellVM, generateTestState } from '../mainState';
import { forceLoad } from '../transforms';
import { isAllowedAction, isAllowedMessage, postActionToExtension } from './helpers';
import { generatePostOfficeSendReducer } from './postOffice';
import { generateMonacoReducer, IMonacoState } from './reducers/monaco';
import { CommonActionType } from './reducers/types';
import { generateVariableReducer, IVariableState } from './reducers/variables';
function generateDefaultState(
skipDefault: boolean,
testMode: boolean,
baseTheme: string,
editable: boolean
): IMainState {
if (!skipDefault) {
return generateTestState('', editable);
} else {
return {
// tslint:disable-next-line: no-typeof-undefined
skipDefault,
testMode,
baseTheme: baseTheme,
cellVMs: [],
busy: true,
undoStack: [],
redoStack: [],
submittedText: false,
currentExecutionCount: 0,
debugging: false,
knownDark: false,
dirty: false,
editCellVM: editable ? undefined : createEditableCellVM(0),
isAtBottom: true,
font: {
size: 14,
family: "Consolas, 'Courier New', monospace"
},
codeTheme: Identifiers.GeneratedThemeName,
focusPending: 0,
monacoReady: testMode, // When testing, monaco starts out ready
loaded: false,
kernel: {
kernelName: getLocString('DataScience.noKernel', 'No Kernel'),
serverName: getLocString('DataScience.serverNotStarted', 'Not Started'),
jupyterServerStatus: ServerStatus.NotStarted,
language: PYTHON_LANGUAGE
},
settings: testMode ? getDefaultSettings() : undefined, // When testing, we don't send (or wait) for the real settings.
editorOptions: testMode ? computeEditorOptions(getDefaultSettings()) : undefined,
isNotebookTrusted: true
};
}
}
function generateMainReducer<M>(
skipDefault: boolean,
testMode: boolean,
baseTheme: string,
editable: boolean,
reducerMap: M
): Redux.Reducer<IMainState, QueuableAction<M>> {
// First create our default state.
const defaultState = generateDefaultState(skipDefault, testMode, baseTheme, editable);
// Then combine that with our map of state change message to reducer
return combineReducers<IMainState, M>(defaultState, reducerMap);
}
function createSendInfoMiddleware(): Redux.Middleware<{}, IStore> {
return (store) => (next) => (action) => {
const prevState = store.getState();
const res = next(action);
const afterState = store.getState();
// If the action is part of a sync message, then do not send it to the extension.
const messageType = (action?.payload as BaseReduxActionPayload).messageType ?? MessageType.other;
const isSyncMessage =
(messageType & MessageType.syncAcrossSameNotebooks) === MessageType.syncAcrossSameNotebooks &&
(messageType & MessageType.syncAcrossSameNotebooks) === MessageType.syncWithLiveShare;
if (isSyncMessage) {
return res;
}
// If cell vm count changed or selected cell changed, send the message
if (!action.type || action.type !== CommonActionType.UNMOUNT) {
const currentSelection = getSelectedAndFocusedInfo(afterState.main);
if (
prevState.main.cellVMs.length !== afterState.main.cellVMs.length ||
getSelectedAndFocusedInfo(prevState.main).selectedCellId !== currentSelection.selectedCellId ||
prevState.main.undoStack.length !== afterState.main.undoStack.length ||
prevState.main.redoStack.length !== afterState.main.redoStack.length
) {
postActionToExtension({ queueAction: store.dispatch }, InteractiveWindowMessages.SendInfo, {
cellCount: afterState.main.cellVMs.length,
undoCount: afterState.main.undoStack.length,
redoCount: afterState.main.redoStack.length,
selectedCell: currentSelection.selectedCellId
});
}
}
return res;
};
}
function createTestLogger() {
const logFileEnv = process.env.VSC_PYTHON_WEBVIEW_LOG_FILE;
if (logFileEnv) {
// tslint:disable-next-line: no-require-imports
const log4js = require('log4js') as typeof import('log4js');
const logFilePath = path.isAbsolute(logFileEnv) ? logFileEnv : path.join(EXTENSION_ROOT_DIR, logFileEnv);
log4js.configure({
appenders: { reduxLogger: { type: 'file', filename: logFilePath } },
categories: { default: { appenders: ['reduxLogger'], level: 'debug' } }
});
return log4js.getLogger();
}
}
function createTestMiddleware(): Redux.Middleware<{}, IStore> {
// Make sure all dynamic imports are loaded.
const transformPromise = forceLoad();
// tslint:disable-next-line: cyclomatic-complexity
return (store) => (next) => (action) => {
const prevState = store.getState();
const res = next(action);
const afterState = store.getState();
// tslint:disable-next-line: no-any
const sendMessage = (message: any, payload?: any) => {
setTimeout(() => {
transformPromise
.then(() => postActionToExtension({ queueAction: store.dispatch }, message, payload))
.ignoreErrors();
});
};
if (!action.type || action.type !== CommonActionType.UNMOUNT) {
// Special case for focusing a cell
const previousSelection = getSelectedAndFocusedInfo(prevState.main);
const currentSelection = getSelectedAndFocusedInfo(afterState.main);
if (previousSelection.focusedCellId !== currentSelection.focusedCellId && currentSelection.focusedCellId) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.FocusedCellEditor, { cellId: action.payload.cellId });
}
if (
previousSelection.selectedCellId !== currentSelection.selectedCellId &&
currentSelection.selectedCellId
) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.SelectedCell, { cellId: action.payload.cellId });
}
// Special case for unfocusing a cell
if (previousSelection.focusedCellId !== currentSelection.focusedCellId && !currentSelection.focusedCellId) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.UnfocusedCellEditor);
}
}
// Indicate settings updates
if (!fastDeepEqual(prevState.main.settings, afterState.main.settings)) {
// Send async so happens after render state changes (so our enzyme wrapper is up to date)
sendMessage(InteractiveWindowMessages.SettingsUpdated);
}
// Indicate clean changes
if (prevState.main.dirty && !afterState.main.dirty) {
sendMessage(InteractiveWindowMessages.NotebookClean);
}
// Indicate dirty changes
if (!prevState.main.dirty && afterState.main.dirty) {
sendMessage(InteractiveWindowMessages.NotebookDirty);
}
// Indicate variables complete
if (
(!fastDeepEqual(prevState.variables.variables, afterState.variables.variables) ||
prevState.variables.currentExecutionCount !== afterState.variables.currentExecutionCount ||
prevState.variables.refreshCount !== afterState.variables.refreshCount) &&
action.type === InteractiveWindowMessages.GetVariablesResponse
) {
sendMessage(InteractiveWindowMessages.VariablesComplete);
}
// Indicate update from extension side
if (action.type && action.type === InteractiveWindowMessages.UpdateModel) {
sendMessage(InteractiveWindowMessages.ReceivedUpdateModel);
}
// Special case for rendering complete
if (
action.type &&
action.type === InteractiveWindowMessages.FinishCell &&
action.payload.data &&
action.payload.data.cell.data?.cell_type === 'code'
) {
// Send async so happens after the render is actually finished.
sendMessage(InteractiveWindowMessages.ExecutionRendered);
}
if (
!action.type ||
(action.type !== InteractiveWindowMessages.FinishCell && action.type !== CommonActionType.UNMOUNT)
) {
// Might be a non finish but still update cells (like an undo or a delete)
const prevFinished = prevState.main.cellVMs
.filter((c) => c.cell.state === CellState.finished || c.cell.state === CellState.error)
.map((c) => c.cell.id);
const afterFinished = afterState.main.cellVMs
.filter((c) => c.cell.state === CellState.finished || c.cell.state === CellState.error)
.map((c) => c.cell.id);
if (
afterFinished.length > prevFinished.length ||
(afterFinished.length !== prevFinished.length &&
afterState.main.cellVMs.length !== prevState.main.cellVMs.length)
) {
// Send async so happens after the render is actually finished.
sendMessage(InteractiveWindowMessages.ExecutionRendered);
}
}
// Hiding/displaying output
const prevHidingOutput = prevState.main.cellVMs.filter((c) => c.hideOutput).map((c) => c.cell.id);
const afterHidingOutput = afterState.main.cellVMs.filter((c) => c.hideOutput).map((c) => c.cell.id);
if (!fastDeepEqual(prevHidingOutput, afterHidingOutput)) {
// Send async so happens after the render is actually finished.
sendMessage(InteractiveWindowMessages.OutputToggled);
}
// Entering break state in a native cell
const prevBreak = prevState.main.cellVMs.find((cvm) => cvm.currentStack);
const newBreak = afterState.main.cellVMs.find((cvm) => cvm.currentStack);
if (prevBreak !== newBreak || !fastDeepEqual(prevBreak?.currentStack, newBreak?.currentStack)) {
sendMessage(InteractiveWindowMessages.ShowingIp);
}
// Kernel state changing
const afterKernel = afterState.main.kernel;
const prevKernel = prevState.main.kernel;
if (
afterKernel.jupyterServerStatus !== prevKernel.jupyterServerStatus &&
afterKernel.jupyterServerStatus === ServerStatus.Idle
) {
sendMessage(InteractiveWindowMessages.KernelIdle);
}
// Debug state changing
const oldState = getDebugState(prevState.main.cellVMs);
const newState = getDebugState(afterState.main.cellVMs);
if (oldState !== newState) {
sendMessage(InteractiveWindowMessages.DebugStateChange, { oldState, newState });
}
if (action.type !== 'action.postOutgoingMessage') {
sendMessage(`DISPATCHED_ACTION_${action.type}`, {});
}
return res;
};
}
// Find the debug state for cell view models
function getDebugState(vms: ICellViewModel[]): DebugState {
const firstNonDesign = vms.find((cvm) => activeDebugState(cvm.runningByLine));
return firstNonDesign ? firstNonDesign.runningByLine : DebugState.Design;
}
function createMiddleWare(testMode: boolean, postOffice: PostOffice): Redux.Middleware<{}, IStore>[] {
// Create the middleware that modifies actions to queue new actions
const queueableActions = createQueueableActionMiddleware();
// Create the update context middle ware. It handles the 'sendInfo' message that
// requires sending on every cell vm length change
const updateContext = createSendInfoMiddleware();
// Create the test middle ware. It sends messages that are used for testing only
// Or if testing in UI Test.
// tslint:disable-next-line: no-any
const isUITest = postOffice.vscodeApi && (postOffice.vscodeApi as any).handleMessage ? true : false;
const testMiddleware = testMode || isUITest ? createTestMiddleware() : undefined;
// Create the logger if we're not in production mode or we're forcing logging
const reduceLogMessage = '<payload too large to displayed in logs (at least on CI)>';
const actionsWithLargePayload = [
InteractiveWindowMessages.LoadOnigasmAssemblyResponse,
CssMessages.GetCssResponse,
InteractiveWindowMessages.LoadTmLanguageResponse
];
const logger = createLogger({
// tslint:disable-next-line: no-any
stateTransformer: (state: any) => {
if (!state || typeof state !== 'object') {
return state;
}
// tslint:disable-next-line: no-any
const rootState = { ...state } as any;
if ('main' in rootState && typeof rootState.main === 'object') {
// tslint:disable-next-line: no-any
const main = (rootState.main = ({ ...rootState.main } as any) as Partial<IMainState>);
main.rootCss = reduceLogMessage;
main.rootStyle = reduceLogMessage;
// tslint:disable-next-line: no-any
main.editorOptions = reduceLogMessage as any;
// tslint:disable-next-line: no-any
main.settings = reduceLogMessage as any;
}
rootState.monaco = reduceLogMessage;
return rootState;
},
// tslint:disable-next-line: no-any
actionTransformer: (action: any) => {
if (!action) {
return action;
}
if (actionsWithLargePayload.indexOf(action.type) >= 0) {
return { ...action, payload: reduceLogMessage };
}
return action;
},
logger: testMode ? createTestLogger() : window.console
});
const loggerMiddleware =
process.env.VSC_PYTHON_FORCE_LOGGING !== undefined && !process.env.VSC_PYTHON_DS_NO_REDUX_LOGGING
? logger
: undefined;
const results: Redux.Middleware<{}, IStore>[] = [];
results.push(queueableActions);
results.push(updateContext);
if (testMiddleware) {
results.push(testMiddleware);
}
if (loggerMiddleware) {
results.push(loggerMiddleware);
}
return results;
}
export interface IStore {
main: IMainState;
variables: IVariableState;
monaco: IMonacoState;
post: {};
}
export interface IMainWithVariables extends IMainState {
variableState: IVariableState;
}
/**
* Middleware that will ensure all actions have `messageDirection` property.
*/
const addMessageDirectionMiddleware: Redux.Middleware = (_store) => (next) => (action: Redux.AnyAction) => {
if (isAllowedAction(action)) {
// Ensure all dispatched messages have been flagged as `incoming`.
const payload: BaseReduxActionPayload<{}> = action.payload || {};
if (!payload.messageDirection) {
action.payload = { ...payload, messageDirection: 'incoming' };
}
}
return next(action);
};
export function createStore<M>(
skipDefault: boolean,
baseTheme: string,
testMode: boolean,
editable: boolean,
showVariablesOnDebug: boolean,
reducerMap: M,
postOffice: PostOffice
) {
// Create reducer for the main react UI
const mainReducer = generateMainReducer(skipDefault, testMode, baseTheme, editable, reducerMap);
// Create reducer to pass window messages to the other side
const postOfficeReducer = generatePostOfficeSendReducer(postOffice);
// Create another reducer for handling monaco state
const monacoReducer = generateMonacoReducer(testMode, postOffice);
// Create another reducer for handling variable state
const variableReducer = generateVariableReducer(showVariablesOnDebug);
// Combine these together
const rootReducer = Redux.combineReducers<IStore>({
main: mainReducer,
variables: variableReducer,
monaco: monacoReducer,
post: postOfficeReducer
});
// Create our middleware
const middleware = createMiddleWare(testMode, postOffice).concat([addMessageDirectionMiddleware]);
// Use this reducer and middle ware to create a store
const store = Redux.createStore(rootReducer, Redux.applyMiddleware(...middleware));
// Make all messages from the post office dispatch to the store, changing the type to
// turn them into actions.
postOffice.addHandler({
// tslint:disable-next-line: no-any
handleMessage(message: string, payload?: any): boolean {
// Double check this is one of our messages. React will actually post messages here too during development
if (isAllowedMessage(message)) {
const basePayload: BaseReduxActionPayload = { data: payload };
if (message === InteractiveWindowMessages.Sync) {
// This is a message that has been sent from extension purely for synchronization purposes.
// Unwrap the message.
message = payload.type;
// This is a message that came in as a result of an outgoing message from another view.
basePayload.messageDirection = 'outgoing';
basePayload.messageType = payload.payload.messageType ?? MessageType.syncAcrossSameNotebooks;
basePayload.data = payload.payload.data;
} else {
// Messages result of some user action.
basePayload.messageType = basePayload.messageType ?? MessageType.other;
}
store.dispatch({ type: message, payload: basePayload });
}
return true;
}
});
return store;
}