Skip to content

Commit 4ddac10

Browse files
committed
feat(python): python code intelligence via Pyrefly with Ruff formatting
- New PythonSupport default extension (desktop only): completion, hover, signature help, jump-to-definition, references, type diagnostics and quick fixes through the shared LSP framework, powered by an exact-pinned Pyrefly wheel installed on demand from PyPI (sha256-verified, node-side download + stdlib zip extraction, no npm or Python runtime needed) - Ruff installs alongside (own pin, independent upgrade) and powers only the Beautify command as a standalone 'ruff format' stdin call - Generic NodeUtils: downloadFile (streamed, progress events, sha256), extractZipFile (stdlib unzipper, restores exec bits), setExecutableBits, execFileWithInput - LSP framework: per-server workspaceConfiguration served to workspace/configuration pulls (pyrefly treats the null fallback answer as all-diagnostics-off) - Install UX shared with PHP: once-per-lifetime dialog over the find-bar prompt (terse line + benefits hover card + powered-by links + download size beside the action buttons); bar styles generalized to lsp-install-* - Fix buttons (problems panel + hover quick view) now show the LSP code action title as tooltip so users see what a fix will apply - integration:Python LSP suite (7 specs) + fixtures
1 parent e05211f commit 4ddac10

21 files changed

Lines changed: 1699 additions & 32 deletions

File tree

docs/API-Reference/language/CodeInspection.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Each error object in the results should have the following structure:
164164
type:?Type ,
165165
fix: { // an optional fix, if present will show the fix button
166166
replaceText: "text to replace the offset given below",
167+
title: "optional tooltip describing what the fix does",
167168
rangeOffset: {
168169
start: number,
169170
end: number
@@ -195,6 +196,7 @@ Each error object in the results should have the following structure:
195196
| type | [<code>Type</code>](#Type) | The type of the error. Defaults to `Type.WARNING` if unspecified. |
196197
| fix | <code>Object</code> | An optional fix object. |
197198
| fix.replaceText | <code>string</code> | The text to replace the error with. |
199+
| fix.title | <code>string</code> | Optional tooltip on the Fix button describing what the fix does. |
198200
| fix.rangeOffset | <code>Object</code> | The range within the text to replace. |
199201
| fix.rangeOffset.start | <code>number</code> | The start offset of the range. |
200202
| fix.rangeOffset.end | <code>number</code> | The end offset of the range. If no errors are found, return either `null`(treated as file is problem free) or an object with a zero-length `errors` array. Always use `message` to safely display the error as text. If you want to display HTML error message, then explicitly use `htmlMessage` to display it. Both `message` and `htmlMessage` can be used simultaneously. After scanning the file, if you need to omit the lint result, return or resolve with `{isIgnored: true}`. This prevents the file from being marked with a no errors tick mark in the status bar and excludes the linter from the problems panel. |

docs/API-Reference/utils/NodeUtils.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,72 @@ This is only available in the native app.
5656
| url | <code>string</code> |
5757
| browserName | <code>string</code> |
5858

59+
<a name="downloadFile"></a>
60+
61+
## downloadFile(url, destFile, [options]) ⇒ <code>Promise.&lt;void&gt;</code>
62+
Downloads a URL to a file on disk, fully node-side (native fetch, streamed to disk).
63+
When an expected sha256 is given, a mismatch deletes the file and rejects - a resolved
64+
promise means the file holds exactly the pinned bytes.
65+
This is only available in the native app.
66+
67+
**Kind**: global function
68+
69+
| Param | Type | Description |
70+
| --- | --- | --- |
71+
| url | <code>string</code> | download URL (redirects followed) |
72+
| destFile | <code>string</code> | platform path to write (parent directories created) |
73+
| [options] | <code>Object</code> | |
74+
| [options.sha256] | <code>string</code> | expected hex digest of the downloaded bytes |
75+
| [options.progress] | <code>function</code> | called with (transferredBytes, totalBytes) as the download advances; totalBytes is 0 if the server sent no length |
76+
77+
<a name="extractZipFile"></a>
78+
79+
## extractZipFile(zipPath, destDir) ⇒ <code>Promise.&lt;void&gt;</code>
80+
Extracts a zip file into a directory node-side (stdlib only, no browser JSZip; creates the
81+
directory if missing, restores unix executable bits recorded in the archive). Python wheels
82+
are plain zips, so this installs those too.
83+
This is only available in the native app.
84+
85+
**Kind**: global function
86+
87+
| Param | Type | Description |
88+
| --- | --- | --- |
89+
| zipPath | <code>string</code> | platform path of the zip file |
90+
| destDir | <code>string</code> | platform path of the directory to extract into |
91+
92+
<a name="setExecutableBits"></a>
93+
94+
## setExecutableBits(filePath) ⇒ <code>Promise.&lt;void&gt;</code>
95+
Marks a file as executable (chmod 755); no-op on Windows. For binaries whose archives did
96+
not carry unix mode bits.
97+
This is only available in the native app.
98+
99+
**Kind**: global function
100+
101+
| Param | Type | Description |
102+
| --- | --- | --- |
103+
| filePath | <code>string</code> | platform path of the file |
104+
105+
<a name="execFileWithInput"></a>
106+
107+
## execFileWithInput(command, [args], [options]) ⇒ <code>Promise.&lt;{code: number, stdout: string, stderr: string}&gt;</code>
108+
Runs an executable with the given args, feeding it text on stdin and capturing its output -
109+
a one-shot filter-style invocation (e.g. `ruff format -` for the Python beautifier). No
110+
shell is involved. Resolves with the exit code rather than rejecting on non-zero, so
111+
callers can read stderr for the reason.
112+
This is only available in the native app.
113+
114+
**Kind**: global function
115+
116+
| Param | Type | Description |
117+
| --- | --- | --- |
118+
| command | <code>string</code> | platform path of the executable (or a PATH command name) |
119+
| [args] | <code>Array.&lt;string&gt;</code> | |
120+
| [options] | <code>Object</code> | |
121+
| [options.stdinText] | <code>string</code> | written to the process's stdin, then closed |
122+
| [options.cwd] | <code>string</code> | working directory |
123+
| [options.timeoutMs] | <code>number</code> | kill the process and reject after this long |
124+
59125
<a name="getEnvironmentVariable"></a>
60126

61127
## getEnvironmentVariable(varName) ⇒ <code>Promise.&lt;string&gt;</code>

gulpfile.js/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -909,8 +909,8 @@ function _renameExtensionConcatAsExtensionJSInDist(extensionName) {
909909
}
910910

911911
const minifyableExtensions = ["CloseOthers", "CodeFolding", "DebugCommands", "Git",
912-
"HealthData", "JavaScriptCodeHints", "JavaScriptRefactoring", "PHPSupport", "QuickView",
913-
"TypeScriptSupport"];
912+
"HealthData", "JavaScriptCodeHints", "JavaScriptRefactoring", "PHPSupport", "PythonSupport",
913+
"QuickView", "TypeScriptSupport"];
914914
// extensions that nned not be minified either coz they are single file extensions or some other reason.
915915
const nonMinifyExtensions = ["CSSAtRuleCodeHints", "CSSCodeHints",
916916
"CSSPseudoSelectorHints", "DarkTheme", "DocCommentHints", "HandlebarsSupport", "HTMLCodeHints",

src-node/lsp-client.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,37 @@ function handleMessage(serverId, msg) {
153153
}
154154
}
155155

156+
/**
157+
* Resolve a workspace/configuration item's dotted section (e.g. "python" or "python.pyrefly")
158+
* inside a server's registered workspaceConfiguration object. Returns null when any path
159+
* segment is missing - the spec's "no config" answer.
160+
* @param {?Object} config - the server's workspaceConfiguration (may be undefined)
161+
* @param {?string} section - the requested section; no section means the whole object
162+
* @return {*}
163+
*/
164+
function _lookupConfigSection(config, section) {
165+
if (!config) {
166+
return null;
167+
}
168+
if (!section) {
169+
return config;
170+
}
171+
let value = config;
172+
for (const part of section.split('.')) {
173+
if (value === null || typeof value !== 'object' || !(part in value)) {
174+
return null;
175+
}
176+
value = value[part];
177+
}
178+
return value;
179+
}
180+
156181
/**
157182
* Answer a server-initiated request with a benign, spec-shaped reply. We advertise minimal client
158183
* capabilities (no dynamic registration, workspace.configuration=false), so servers should rarely
159184
* send these - this is the safety net that guarantees no server hangs awaiting a reply.
185+
* Servers that pull configuration regardless (e.g. pyrefly) get the workspaceConfiguration
186+
* object registered at startServer; anything else gets the spec's null "no config".
160187
* @param {string} serverId - The server identifier (for logging)
161188
* @param {Object} server - The server state object
162189
* @param {Object} msg - The incoming JSON-RPC request (method + id)
@@ -168,7 +195,8 @@ function _respondToServerRequest(serverId, server, msg) {
168195
// Result must be an array matching params.items length; null entries mean "no config".
169196
response = {
170197
jsonrpc: '2.0', id: msg.id,
171-
result: ((msg.params && msg.params.items) || []).map(() => null)
198+
result: ((msg.params && msg.params.items) || []).map(
199+
item => _lookupConfigSection(server.workspaceConfiguration, item && item.section))
172200
};
173201
break;
174202
case 'client/registerCapability':
@@ -205,10 +233,13 @@ exports.ping = async function ping() {
205233
* @param {string} params.command - Command used to spawn the language server
206234
* @param {string[]} [params.args=['--stdio']] - Arguments for the command
207235
* @param {string} params.rootUri - Root URI of the workspace
236+
* @param {Object} [params.workspaceConfiguration] - settings tree served to the server's
237+
* workspace/configuration pulls (sections resolved by dotted path); pulls answer null
238+
* without it
208239
* @returns {Promise<Object>} Result with success status and server info
209240
*/
210241
exports.startServer = async function startServer(params) {
211-
const { serverId, command, args = ['--stdio'], rootUri } = params;
242+
const { serverId, command, args = ['--stdio'], rootUri, workspaceConfiguration } = params;
212243

213244
if (!serverId || !command) {
214245
throw new Error('serverId and command are required');
@@ -224,7 +255,9 @@ exports.startServer = async function startServer(params) {
224255
// same spawn-self pattern _npmInstallInFolder and the ESLint service use. This sidesteps
225256
// node_modules/.bin shims entirely (they are sh scripts / .cmd on Windows).
226257
// 2. A server bundled in src-node/node_modules/.bin.
227-
// 3. Fall back to PATH.
258+
// 3. Fall back to spawning the command as given - which also covers an absolute path to a
259+
// native binary (e.g. a user-installed server like pyrefly), spawned as-is, or a PATH
260+
// lookup for a bare command name.
228261
let commandPath = command;
229262
let spawnArgs = args;
230263
if (path.isAbsolute(command) && command.endsWith('.js')) {
@@ -245,6 +278,7 @@ exports.startServer = async function startServer(params) {
245278
process: serverProcess,
246279
pending: new Map(),
247280
rootUri,
281+
workspaceConfiguration,
248282
stderrTail: [] // keep the last few stderr lines to attach to crash reports
249283
};
250284

0 commit comments

Comments
 (0)