@@ -5,9 +5,11 @@ import '../../common/extensions';
5
5
import { Disposable , LanguageClient , LanguageClientOptions } from 'vscode-languageclient/node' ;
6
6
7
7
// --- Start Positron ---
8
+ import * as positron from 'positron' ;
9
+ import * as vscode from 'vscode' ;
8
10
import * as path from 'path' ;
9
11
import { EXTENSION_ROOT_DIR } from '../../common/constants' ;
10
- import { ChildProcess , spawn , SpawnOptions } from 'child_process' ;
12
+ import { ChildProcess } from 'child_process' ;
11
13
// --- End Positron ---
12
14
import { Resource } from '../../common/types' ;
13
15
import { PythonEnvironment } from '../../pythonEnvironments/info' ;
@@ -20,23 +22,35 @@ import { ILanguageServerProxy } from '../types';
20
22
import { killPid } from '../../common/process/rawProcessApis' ;
21
23
import { JediLanguageClientFactory } from './languageClientFactory' ;
22
24
import { IInterpreterService } from '../../interpreter/contracts' ;
23
- import { traceDecoratorError , traceDecoratorVerbose , traceError , traceInfo , traceVerbose } from '../../logging' ;
25
+ import { traceDecoratorError , traceDecoratorVerbose , traceError , traceVerbose , traceWarn } from '../../logging' ;
26
+ import { IServiceContainer } from '../../ioc/types' ;
27
+ import { IPythonExecutionFactory } from '../../common/process/types' ;
24
28
// --- End Positron ---
25
29
26
30
export class JediLanguageServerProxy implements ILanguageServerProxy {
27
31
private languageClient : LanguageClient | undefined ;
28
32
// --- Start Positron ---
29
- private serverProcess : ChildProcess | undefined ;
33
+ private extensionVersion : string | undefined ;
30
34
// --- End Positron ---
31
35
private readonly disposables : Disposable [ ] = [ ] ;
32
36
33
37
private lsVersion : string | undefined ;
34
38
35
39
// --- Start Positron ---
36
40
constructor (
41
+ private readonly serviceContainer : IServiceContainer ,
37
42
private readonly interpreterService : IInterpreterService ,
38
43
private readonly factory : JediLanguageClientFactory
39
- ) { }
44
+ ) {
45
+ // Get the version of this extension from package.json so that we can
46
+ // describe the implementation version to the kernel adapter
47
+ try {
48
+ const packageJson = require ( '../../../../package.json' ) ;
49
+ this . extensionVersion = packageJson . version ;
50
+ } catch ( e ) {
51
+ traceVerbose ( "Unable to read package.json to determine our extension version" , e ) ;
52
+ }
53
+ }
40
54
// --- End Positron ---
41
55
42
56
private static versionTelemetryProps ( instance : JediLanguageServerProxy ) {
@@ -69,53 +83,112 @@ export class JediLanguageServerProxy implements ILanguageServerProxy {
69
83
70
84
try {
71
85
// --- Start Positron ---
72
- const port = await this . startLSPAndKernel ( resource , interpreter ) ;
73
- const client = await this . factory . createLanguageClientTCP ( port , options ) ;
74
- // TODO: Ask Jupyter Adapter to attach to our kernel
86
+
87
+ // Favor the active interpreter, if one is available
88
+ const activeInterpreter = await this . interpreterService . getActiveInterpreter ( resource ) ;
89
+ const targetInterpreter = activeInterpreter ? activeInterpreter : interpreter ;
90
+
91
+ // Determine if our Jupyter Adapter extension is installed
92
+ const ext = vscode . extensions . getExtension ( 'vscode.jupyter-adapter' ) ;
93
+ const hasKernel = await this . hasIpyKernelModule ( targetInterpreter , this . serviceContainer ) ;
94
+ if ( ext && hasKernel ) {
95
+
96
+ // If our adapter is installed, and if the active Python interpreter has the IPyKernel module
97
+ // installed, we'll use it to manage our language runtime. It will start a combined LSP and
98
+ // IPyKernel server, providing enhanced code insights to the Editor and supports our
99
+ // Python REPL console. The language client will connect to the server via TCP.
100
+ this . withActiveExtention ( ext , async ( ) => {
101
+ const disposable : vscode . Disposable = await this . registerLanguageRuntime ( ext , targetInterpreter , options , hasKernel ) ;
102
+ this . disposables . push ( disposable ) ;
103
+ } ) ;
104
+
105
+ } else {
106
+
107
+ // Otherwise, use the default Jedi LSP for the Editor
108
+ traceWarn ( 'Could not find Jupyter Adapter extension to register an enhanced Python runtime. Creating an LSP only.' ) ;
109
+ const client = await this . factory . createLanguageClient ( resource , targetInterpreter , options ) ;
110
+ this . startClient ( client ) ;
111
+ }
75
112
// --- End Positron ---
76
- this . registerHandlers ( client ) ;
77
- await client . start ( ) ;
78
- this . languageClient = client ;
79
113
} catch ( ex ) {
80
114
traceError ( 'Failed to start language server:' , ex ) ;
81
115
throw new Error ( 'Launching Jedi language server using python failed, see output.' ) ;
82
116
}
83
117
}
84
118
85
119
// --- Start Positron ---
120
+
121
+ /**
122
+ * Checks if a given python environment has the ipykernel module installed.
123
+ */
124
+ private async hasIpyKernelModule ( interpreter : PythonEnvironment | undefined , serviceContainer : IServiceContainer ) : Promise < boolean > {
125
+ if ( ! interpreter ) { return false ; }
126
+ const pythonFactory = serviceContainer . get < IPythonExecutionFactory > ( IPythonExecutionFactory ) ;
127
+ let pythonService = await pythonFactory . create ( { pythonPath : interpreter . path } ) ;
128
+ return pythonService . isModuleInstalled ( 'ipykernel' ) ;
129
+ }
130
+
86
131
/**
87
- * Finds an available port and starts a Jedi LSP as a TCP server, including an IPyKernel.
132
+ * Register our Jedi LSP as a language runtime with our Jupyter Adapter extension.
133
+ * The LSP will find an available port to start via TCP, and the Jupyter Adapter will configure
134
+ * IPyKernel with a connection file.
88
135
*/
89
- private async startLSPAndKernel ( resource : Resource , _interpreter : PythonEnvironment | undefined ) : Promise < number > {
136
+ private async registerLanguageRuntime ( ext : vscode . Extension < any > , interpreter : PythonEnvironment | undefined , options : LanguageClientOptions , hasKernel : boolean ) : Promise < Disposable > {
90
137
91
138
// Find an available port for our TCP server
92
139
const portfinder = require ( 'portfinder' ) ;
93
140
const port = await portfinder . getPortPromise ( ) ;
94
141
95
- // For now, match vscode behavior and always look up the active interpreter each time
96
- const interpreter = await this . interpreterService . getActiveInterpreter ( resource ) ;
142
+ // Customize Jedi LSP entrypoint that adds a resident IPyKernel
97
143
const command = interpreter ? interpreter . path : 'python' ;
98
-
99
- // Customize Jedi entrypoint with an additional resident IPyKernel
144
+ const pythonVersion = interpreter ?. version ?. raw ;
100
145
const lsScriptPath = path . join ( EXTENSION_ROOT_DIR , 'pythonFiles' , 'ipykernel_jedi.py' ) ;
101
- const args = [ lsScriptPath , `--port=${ port } ` ] // '-f', '{ connection_file }']
102
- traceVerbose ( `Configuring Jedi LSP server with args '${ args } '` ) ;
103
-
104
- // Spawn Jedi LSP in TCP mode
105
- const options : SpawnOptions = { env : { } } ;
106
- const process : ChildProcess = spawn ( command , args , options ) ;
107
- if ( ! process || ! process . pid ) {
108
- return Promise . reject ( `Failed to spawn Jedi LSP server using command '${ command } ' with args '${ args } '.` ) ;
109
- }
146
+ const args = [ command , lsScriptPath , `--port=${ port } ` , '-f' , '{connection_file}' , '--logfile' , '{log_file}' ]
147
+ const kernelSpec = {
148
+ argv : args ,
149
+ display_name : `${ interpreter ?. displayName } (ipykernel)` ,
150
+ language : 'Python' ,
151
+ metadata : { debugger : false }
152
+ } ;
153
+ traceVerbose ( `Configuring Jedi LSP with IPyKernel using args '${ args } '` ) ;
110
154
111
- // Wait for spawn to complete
112
- await new Promise ( ( resolve ) => {
113
- process . once ( 'spawn ', ( ) => { resolve ( true ) ; } ) ;
114
- } ) ;
115
- traceInfo ( `Spawned Jedi LSP on port ' ${ port } ' with pid ' ${ process . pid } '` ) ;
116
- this . serverProcess = process ;
155
+ // Create an adapter for the kernel as our language runtime
156
+ const startupBehavior = hasKernel ? positron . LanguageRuntimeStartupBehavior . Implicit : positron . LanguageRuntimeStartupBehavior . Explicit ;
157
+ const runtime = ext . exports . adaptKernel ( kernelSpec , 'Python ', pythonVersion , this . extensionVersion , ( ) => {
158
+ // The adapter will create a language client to connect to the LSP via TCP
159
+ return this . activateClientTCP ( port , options ) ;
160
+ } , startupBehavior ) ;
117
161
118
- return port ;
162
+ // Register our language runtime provider
163
+ return positron . runtime . registerLanguageRuntime ( runtime ) ;
164
+ }
165
+
166
+ /**
167
+ * Creates and starts a language client to connect to our LSP via TCP
168
+ */
169
+ private async activateClientTCP ( port : number , options : LanguageClientOptions ) : Promise < void > {
170
+ const client = await this . factory . createLanguageClientTCP ( port , options ) ;
171
+ this . startClient ( client ) ;
172
+ }
173
+
174
+ /**
175
+ * Starts the language client and registers it for disposal
176
+ */
177
+ private async startClient ( client : LanguageClient ) : Promise < void > {
178
+ this . registerHandlers ( client ) ;
179
+ await client . start ( ) ;
180
+ this . languageClient = client ;
181
+ }
182
+
183
+ /**
184
+ * Utility to ensure an extension is active before an action is performed
185
+ */
186
+ private withActiveExtention ( ext : vscode . Extension < any > , callback : ( ) => void ) {
187
+ if ( ext . isActive ) {
188
+ callback ( ) ;
189
+ } else {
190
+ ext . activate ( ) . then ( callback ) ;
191
+ }
119
192
}
120
193
// --- End Positron ---
121
194
@@ -126,17 +199,6 @@ export class JediLanguageServerProxy implements ILanguageServerProxy {
126
199
d . dispose ( ) ;
127
200
}
128
201
129
- // --- Start Positron ---
130
- // If we spawned our own process, stop it
131
- if ( this . serverProcess ?. pid ) {
132
- try {
133
- killPid ( this . serverProcess . pid ) ;
134
- } catch ( ex ) {
135
- traceError ( 'Stopping Jedi language server failed' , ex ) ;
136
- }
137
- }
138
- // --- End Positron ---
139
-
140
202
if ( this . languageClient ) {
141
203
const client = this . languageClient ;
142
204
this . languageClient = undefined ;
0 commit comments