Skip to content

Commit 8ac716d

Browse files
authored
Handle reloading of REPL Window (#24148)
Resolves: #24021
1 parent 7e9b927 commit 8ac716d

File tree

6 files changed

+114
-23
lines changed

6 files changed

+114
-23
lines changed

src/client/repl/nativeRepl.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
NotebookDocument,
77
QuickPickItem,
88
TextEditor,
9+
Uri,
910
workspace,
1011
WorkspaceFolder,
1112
} from 'vscode';
@@ -21,8 +22,11 @@ import { EventName } from '../telemetry/constants';
2122
import { sendTelemetryEvent } from '../telemetry';
2223
import { VariablesProvider } from './variables/variablesProvider';
2324
import { VariableRequester } from './variables/variableRequester';
25+
import { getTabNameForUri } from './replUtils';
26+
import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../common/persistentState';
2427

25-
let nativeRepl: NativeRepl | undefined; // In multi REPL scenario, hashmap of URI to Repl.
28+
export const NATIVE_REPL_URI_MEMENTO = 'nativeReplUri';
29+
let nativeRepl: NativeRepl | undefined;
2630
export class NativeRepl implements Disposable {
2731
// Adding ! since it will get initialized in create method, not the constructor.
2832
private pythonServer!: PythonServer;
@@ -65,10 +69,11 @@ export class NativeRepl implements Disposable {
6569
*/
6670
private watchNotebookClosed(): void {
6771
this.disposables.push(
68-
workspace.onDidCloseNotebookDocument((nb) => {
72+
workspace.onDidCloseNotebookDocument(async (nb) => {
6973
if (this.notebookDocument && nb.uri.toString() === this.notebookDocument.uri.toString()) {
7074
this.notebookDocument = undefined;
7175
this.newReplSession = true;
76+
await updateWorkspaceStateValue<string | undefined>(NATIVE_REPL_URI_MEMENTO, undefined);
7277
}
7378
}),
7479
);
@@ -145,15 +150,37 @@ export class NativeRepl implements Disposable {
145150
/**
146151
* Function that opens interactive repl, selects kernel, and send/execute code to the native repl.
147152
*/
148-
public async sendToNativeRepl(code?: string): Promise<void> {
149-
const notebookEditor = await openInteractiveREPL(this.replController, this.notebookDocument);
150-
this.notebookDocument = notebookEditor.notebook;
151-
152-
if (this.notebookDocument) {
153-
this.replController.updateNotebookAffinity(this.notebookDocument, NotebookControllerAffinity.Default);
154-
await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID);
155-
if (code) {
156-
await executeNotebookCell(notebookEditor, code);
153+
public async sendToNativeRepl(code?: string | undefined, preserveFocus: boolean = true): Promise<void> {
154+
let wsMementoUri: Uri | undefined;
155+
156+
if (!this.notebookDocument) {
157+
const wsMemento = getWorkspaceStateValue<string>(NATIVE_REPL_URI_MEMENTO);
158+
wsMementoUri = wsMemento ? Uri.parse(wsMemento) : undefined;
159+
160+
if (!wsMementoUri || getTabNameForUri(wsMementoUri) !== 'Python REPL') {
161+
await updateWorkspaceStateValue<string | undefined>(NATIVE_REPL_URI_MEMENTO, undefined);
162+
wsMementoUri = undefined;
163+
}
164+
}
165+
166+
const notebookEditor = await openInteractiveREPL(
167+
this.replController,
168+
this.notebookDocument ?? wsMementoUri,
169+
preserveFocus,
170+
);
171+
if (notebookEditor) {
172+
this.notebookDocument = notebookEditor.notebook;
173+
await updateWorkspaceStateValue<string | undefined>(
174+
NATIVE_REPL_URI_MEMENTO,
175+
this.notebookDocument.uri.toString(),
176+
);
177+
178+
if (this.notebookDocument) {
179+
this.replController.updateNotebookAffinity(this.notebookDocument, NotebookControllerAffinity.Default);
180+
await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID);
181+
if (code) {
182+
await executeNotebookCell(notebookEditor, code);
183+
}
157184
}
158185
}
159186
}

src/client/repl/replCommandHandler.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,48 @@ import {
1010
NotebookEdit,
1111
WorkspaceEdit,
1212
workspace,
13+
Uri,
1314
} from 'vscode';
14-
import { getExistingReplViewColumn } from './replUtils';
15+
import { getExistingReplViewColumn, getTabNameForUri } from './replUtils';
1516
import { PVSC_EXTENSION_ID } from '../common/constants';
1617

1718
/**
1819
* Function that opens/show REPL using IW UI.
1920
*/
2021
export async function openInteractiveREPL(
2122
notebookController: NotebookController,
22-
notebookDocument: NotebookDocument | undefined,
23-
): Promise<NotebookEditor> {
23+
notebookDocument: NotebookDocument | Uri | undefined,
24+
preserveFocus: boolean = true,
25+
): Promise<NotebookEditor | undefined> {
2426
let viewColumn = ViewColumn.Beside;
25-
26-
// Case where NotebookDocument (REPL document already exists in the tab)
27-
if (notebookDocument) {
27+
if (notebookDocument instanceof Uri) {
28+
// Case where NotebookDocument is undefined, but workspace mementoURI exists.
29+
notebookDocument = await workspace.openNotebookDocument(notebookDocument);
30+
} else if (notebookDocument) {
31+
// Case where NotebookDocument (REPL document already exists in the tab)
2832
const existingReplViewColumn = getExistingReplViewColumn(notebookDocument);
2933
viewColumn = existingReplViewColumn ?? viewColumn;
3034
} else if (!notebookDocument) {
31-
// Case where NotebookDocument doesnt exist, create a blank one.
35+
// Case where NotebookDocument doesnt exist, or
36+
// became outdated (untitled.ipynb created without Python extension knowing, effectively taking over original Python REPL's URI)
3237
notebookDocument = await workspace.openNotebookDocument('jupyter-notebook');
3338
}
34-
const editor = window.showNotebookDocument(notebookDocument!, {
39+
const editor = await window.showNotebookDocument(notebookDocument!, {
3540
viewColumn,
3641
asRepl: 'Python REPL',
37-
preserveFocus: true,
42+
preserveFocus,
3843
});
44+
45+
// Sanity check that we opened a Native REPL from showNotebookDocument.
46+
if (
47+
!editor ||
48+
!editor.notebook ||
49+
!editor.notebook.uri ||
50+
getTabNameForUri(editor.notebook.uri) !== 'Python REPL'
51+
) {
52+
return undefined;
53+
}
54+
3955
await commands.executeCommand('notebook.selectKernel', {
4056
editor,
4157
id: notebookController.id,

src/client/repl/replCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function registerStartNativeReplCommand(
3333
if (interpreter) {
3434
if (interpreter) {
3535
const nativeRepl = await getNativeRepl(interpreter, disposables);
36-
await nativeRepl.sendToNativeRepl();
36+
await nativeRepl.sendToNativeRepl(undefined, false);
3737
}
3838
}
3939
}),

src/client/repl/replUtils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,22 @@ export function getExistingReplViewColumn(notebookDocument: NotebookDocument): V
9999
}
100100
return undefined;
101101
}
102+
103+
/**
104+
* Function that will return tab name for before reloading VS Code
105+
* This is so we can make sure tab name is still 'Python REPL' after reloading VS Code,
106+
* and make sure Python REPL does not get 'merged' into unaware untitled.ipynb tab.
107+
*/
108+
export function getTabNameForUri(uri: Uri): string | undefined {
109+
const tabGroups = window.tabGroups.all;
110+
111+
for (const tabGroup of tabGroups) {
112+
for (const tab of tabGroup.tabs) {
113+
if (tab.input instanceof TabInputNotebook && tab.input.uri.toString() === uri.toString()) {
114+
return tab.label;
115+
}
116+
}
117+
}
118+
119+
return undefined;
120+
}

src/test/repl/nativeRepl.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ import { expect } from 'chai';
88
import { IInterpreterService } from '../../client/interpreter/contracts';
99
import { PythonEnvironment } from '../../client/pythonEnvironments/info';
1010
import { getNativeRepl, NativeRepl } from '../../client/repl/nativeRepl';
11+
import * as persistentState from '../../client/common/persistentState';
1112

1213
suite('REPL - Native REPL', () => {
1314
let interpreterService: TypeMoq.IMock<IInterpreterService>;
1415

1516
let disposable: TypeMoq.IMock<Disposable>;
1617
let disposableArray: Disposable[] = [];
17-
1818
let setReplDirectoryStub: sinon.SinonStub;
1919
let setReplControllerSpy: sinon.SinonSpy;
20+
let getWorkspaceStateValueStub: sinon.SinonStub;
21+
let updateWorkspaceStateValueStub: sinon.SinonStub;
2022

2123
setup(() => {
2224
interpreterService = TypeMoq.Mock.ofType<IInterpreterService>();
@@ -29,6 +31,7 @@ suite('REPL - Native REPL', () => {
2931
setReplDirectoryStub = sinon.stub(NativeRepl.prototype as any, 'setReplDirectory').resolves(); // Stubbing private method
3032
// Use a spy instead of a stub for setReplController
3133
setReplControllerSpy = sinon.spy(NativeRepl.prototype, 'setReplController');
34+
updateWorkspaceStateValueStub = sinon.stub(persistentState, 'updateWorkspaceStateValue').resolves();
3235
});
3336

3437
teardown(() => {
@@ -37,7 +40,6 @@ suite('REPL - Native REPL', () => {
3740
d.dispose();
3841
}
3942
});
40-
4143
disposableArray = [];
4244
sinon.restore();
4345
});
@@ -53,6 +55,32 @@ suite('REPL - Native REPL', () => {
5355
expect(createMethodStub.calledOnce).to.be.true;
5456
});
5557

58+
test('sendToNativeRepl should look for memento URI if notebook document is undefined', async () => {
59+
getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns(undefined);
60+
interpreterService
61+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
62+
.returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment));
63+
const interpreter = await interpreterService.object.getActiveInterpreter();
64+
const nativeRepl = await getNativeRepl(interpreter as PythonEnvironment, disposableArray);
65+
66+
nativeRepl.sendToNativeRepl(undefined, false);
67+
68+
expect(getWorkspaceStateValueStub.calledOnce).to.be.true;
69+
});
70+
71+
test('sendToNativeRepl should call updateWorkspaceStateValue', async () => {
72+
getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns('myNameIsMemento');
73+
interpreterService
74+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
75+
.returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment));
76+
const interpreter = await interpreterService.object.getActiveInterpreter();
77+
const nativeRepl = await getNativeRepl(interpreter as PythonEnvironment, disposableArray);
78+
79+
nativeRepl.sendToNativeRepl(undefined, false);
80+
81+
expect(updateWorkspaceStateValueStub.calledOnce).to.be.true;
82+
});
83+
5684
test('create should call setReplDirectory, setReplController', async () => {
5785
const interpreter = await interpreterService.object.getActiveInterpreter();
5886
interpreterService

src/test/repl/replCommand.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ suite('REPL - register native repl command', () => {
2424
let getNativeReplStub: sinon.SinonStub;
2525
let disposable: TypeMoq.IMock<Disposable>;
2626
let disposableArray: Disposable[] = [];
27+
2728
setup(() => {
2829
interpreterService = TypeMoq.Mock.ofType<IInterpreterService>();
2930
commandManager = TypeMoq.Mock.ofType<ICommandManager>();

0 commit comments

Comments
 (0)