Skip to content

Commit 25411e5

Browse files
authored
Launch Native REPL using terminal link (microsoft#24734)
Resolves: microsoft#24270 Perhaps further improve via microsoft#24270 (comment) ?
1 parent 92cc4ed commit 25411e5

File tree

7 files changed

+151
-2
lines changed

7 files changed

+151
-2
lines changed

python_files/pythonrc.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,8 @@ def __str__(self):
7777

7878
if sys.platform != "win32" and (not is_wsl) and use_shell_integration:
7979
sys.ps1 = PS1()
80+
81+
if sys.platform == "darwin":
82+
print("Cmd click to launch VS Code Native REPL")
83+
else:
84+
print("Ctrl click to launch VS Code Native REPL")

python_files/tests/test_shell_integration.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,23 @@ def test_excepthook_call():
6161

6262
hooks.my_excepthook("mock_type", "mock_value", "mock_traceback")
6363
mock_excepthook.assert_called_once_with("mock_type", "mock_value", "mock_traceback")
64+
65+
66+
if sys.platform == "darwin":
67+
68+
def test_print_statement_darwin(monkeypatch):
69+
importlib.reload(pythonrc)
70+
with monkeypatch.context() as m:
71+
m.setattr("builtins.print", Mock())
72+
importlib.reload(sys.modules["pythonrc"])
73+
print.assert_any_call("Cmd click to launch VS Code Native REPL")
74+
75+
76+
if sys.platform == "win32":
77+
78+
def test_print_statement_non_darwin(monkeypatch):
79+
importlib.reload(pythonrc)
80+
with monkeypatch.context() as m:
81+
m.setattr("builtins.print", Mock())
82+
importlib.reload(sys.modules["pythonrc"])
83+
print.assert_any_call("Ctrl click to launch VS Code Native REPL")

src/client/common/utils/localize.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export namespace AttachProcess {
9292

9393
export namespace Repl {
9494
export const disableSmartSend = l10n.t('Disable Smart Send');
95+
export const launchNativeRepl = l10n.t('Launch VS Code Native REPL');
9596
}
9697
export namespace Pylance {
9798
export const remindMeLater = l10n.t('Remind me later');

src/client/common/vscodeApis/windowApis.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
TerminalShellExecutionStartEvent,
2222
LogOutputChannel,
2323
OutputChannel,
24+
TerminalLinkProvider,
2425
} from 'vscode';
2526
import { createDeferred, Deferred } from '../utils/async';
2627
import { Resource } from '../types';
@@ -258,3 +259,7 @@ export function createOutputChannel(name: string, languageId?: string): OutputCh
258259
export function createLogOutputChannel(name: string, options: { log: true }): LogOutputChannel {
259260
return window.createOutputChannel(name, options);
260261
}
262+
263+
export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable {
264+
return window.registerTerminalLinkProvider(provider);
265+
}

src/client/extensionActivation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeRe
5555
import { registerTriggerForTerminalREPL } from './terminals/codeExecution/terminalReplWatcher';
5656
import { registerPythonStartup } from './terminals/pythonStartup';
5757
import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi';
58+
import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider';
5859

5960
export async function activateComponents(
6061
// `ext` is passed to any extra activation funcs.
@@ -115,6 +116,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
115116
registerStartNativeReplCommand(ext.disposables, interpreterService);
116117
registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager);
117118
registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager);
119+
registerCustomTerminalLinkProvider(ext.disposables);
118120
}
119121

120122
/// //////////////////////////
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-disable class-methods-use-this */
2+
import {
3+
CancellationToken,
4+
Disposable,
5+
ProviderResult,
6+
TerminalLink,
7+
TerminalLinkContext,
8+
TerminalLinkProvider,
9+
} from 'vscode';
10+
import { executeCommand } from '../common/vscodeApis/commandApis';
11+
import { registerTerminalLinkProvider } from '../common/vscodeApis/windowApis';
12+
import { Repl } from '../common/utils/localize';
13+
14+
interface CustomTerminalLink extends TerminalLink {
15+
command: string;
16+
}
17+
18+
export class CustomTerminalLinkProvider implements TerminalLinkProvider<CustomTerminalLink> {
19+
provideTerminalLinks(
20+
context: TerminalLinkContext,
21+
_token: CancellationToken,
22+
): ProviderResult<CustomTerminalLink[]> {
23+
const links: CustomTerminalLink[] = [];
24+
const expectedNativeLink = 'VS Code Native REPL';
25+
26+
if (context.line.includes(expectedNativeLink)) {
27+
links.push({
28+
startIndex: context.line.indexOf(expectedNativeLink),
29+
length: expectedNativeLink.length,
30+
tooltip: Repl.launchNativeRepl,
31+
command: 'python.startNativeREPL',
32+
});
33+
}
34+
return links;
35+
}
36+
37+
async handleTerminalLink(link: CustomTerminalLink): Promise<void> {
38+
await executeCommand(link.command);
39+
}
40+
}
41+
42+
export function registerCustomTerminalLinkProvider(disposables: Disposable[]): void {
43+
disposables.push(registerTerminalLinkProvider(new CustomTerminalLinkProvider()));
44+
}

src/test/terminals/shellIntegration/pythonStartup.test.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,23 @@
33

44
import * as sinon from 'sinon';
55
import * as TypeMoq from 'typemoq';
6-
import { GlobalEnvironmentVariableCollection, Uri, WorkspaceConfiguration } from 'vscode';
6+
import {
7+
GlobalEnvironmentVariableCollection,
8+
Uri,
9+
WorkspaceConfiguration,
10+
Disposable,
11+
CancellationToken,
12+
TerminalLinkContext,
13+
Terminal,
14+
EventEmitter,
15+
} from 'vscode';
16+
import { assert } from 'chai';
717
import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis';
818
import { registerPythonStartup } from '../../../client/terminals/pythonStartup';
919
import { IExtensionContext } from '../../../client/common/types';
20+
import * as pythonStartupLinkProvider from '../../../client/terminals/pythonStartupLinkProvider';
21+
import { CustomTerminalLinkProvider } from '../../../client/terminals/pythonStartupLinkProvider';
22+
import { Repl } from '../../../client/common/utils/localize';
1023

1124
suite('Terminal - Shell Integration with PYTHONSTARTUP', () => {
1225
let getConfigurationStub: sinon.SinonStub;
@@ -20,7 +33,6 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => {
2033
setup(() => {
2134
context = TypeMoq.Mock.ofType<IExtensionContext>();
2235
globalEnvironmentVariableCollection = TypeMoq.Mock.ofType<GlobalEnvironmentVariableCollection>();
23-
2436
// Question: Why do we have to set up environmentVariableCollection and globalEnvironmentVariableCollection in this flip-flop way?
2537
// Reference: /vscode-python/src/test/interpreters/activation/terminalEnvVarCollectionService.unit.test.ts
2638
context.setup((c) => c.environmentVariableCollection).returns(() => globalEnvironmentVariableCollection.object);
@@ -122,4 +134,64 @@ suite('Terminal - Shell Integration with PYTHONSTARTUP', () => {
122134

123135
globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.once());
124136
});
137+
138+
test('Ensure registering terminal link calls registerTerminalLinkProvider', async () => {
139+
const registerTerminalLinkProviderStub = sinon.stub(
140+
pythonStartupLinkProvider,
141+
'registerCustomTerminalLinkProvider',
142+
);
143+
const disposableArray: Disposable[] = [];
144+
pythonStartupLinkProvider.registerCustomTerminalLinkProvider(disposableArray);
145+
146+
sinon.assert.calledOnce(registerTerminalLinkProviderStub);
147+
sinon.assert.calledWith(registerTerminalLinkProviderStub, disposableArray);
148+
149+
registerTerminalLinkProviderStub.restore();
150+
});
151+
152+
test('Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => {
153+
const provider = new CustomTerminalLinkProvider();
154+
const context: TerminalLinkContext = {
155+
line: 'Some random string with VS Code Native REPL in it',
156+
terminal: {} as Terminal,
157+
};
158+
const token: CancellationToken = {
159+
isCancellationRequested: false,
160+
onCancellationRequested: new EventEmitter<unknown>().event,
161+
};
162+
163+
const links = provider.provideTerminalLinks(context, token);
164+
165+
assert.isNotNull(links, 'Expected links to be not undefined');
166+
assert.isArray(links, 'Expected links to be an array');
167+
assert.isNotEmpty(links, 'Expected links to be not empty');
168+
169+
if (Array.isArray(links)) {
170+
assert.equal(links[0].command, 'python.startNativeREPL', 'Expected command to be python.startNativeREPL');
171+
assert.equal(
172+
links[0].startIndex,
173+
context.line.indexOf('VS Code Native REPL'),
174+
'Expected startIndex to be 0',
175+
);
176+
assert.equal(links[0].length, 'VS Code Native REPL'.length, 'Expected length to be 16');
177+
assert.equal(links[0].tooltip, Repl.launchNativeRepl, 'Expected tooltip to be Launch VS Code Native REPL');
178+
}
179+
});
180+
181+
test('Verify provideTerminalLinks returns no links when context.line does not contain expectedNativeLink', () => {
182+
const provider = new CustomTerminalLinkProvider();
183+
const context: TerminalLinkContext = {
184+
line: 'Some random string without the expected link',
185+
terminal: {} as Terminal,
186+
};
187+
const token: CancellationToken = {
188+
isCancellationRequested: false,
189+
onCancellationRequested: new EventEmitter<unknown>().event,
190+
};
191+
192+
const links = provider.provideTerminalLinks(context, token);
193+
194+
assert.isArray(links, 'Expected links to be an array');
195+
assert.isEmpty(links, 'Expected links to be empty');
196+
});
125197
});

0 commit comments

Comments
 (0)