From d7b978e5dbe9a777dcb51ce84b6dcf82541a278b Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Fri, 17 Oct 2025 12:44:24 -0700 Subject: [PATCH 1/2] update lsp_types to 0.97.0 Differential Revision: D84942174 --- Cargo.lock | 314 ++----------------------------- crates/pyrefly_python/Cargo.toml | 2 +- crates/pyrefly_util/Cargo.toml | 2 +- crates/tsp_types/Cargo.toml | 2 +- pyrefly/Cargo.toml | 2 +- 5 files changed, 21 insertions(+), 301 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c8bc168dc..3ebe58243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,16 +718,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "git+https://github.com/yaahc/displaydoc?rev=7dc6e324b1788a6b7fb9f3a1953c512923a3e9f0#7dc6e324b1788a6b7fb9f3a1953c512923a3e9f0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "dupe" version = "0.9.1" @@ -830,6 +820,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "fnv" version = "1.0.7" @@ -842,15 +841,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - [[package]] name = "fs-err" version = "2.11.0" @@ -1154,151 +1144,12 @@ dependencies = [ "cxx-build", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "ignore" version = "0.4.23" @@ -1552,12 +1403,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.13" @@ -1599,15 +1444,15 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.94.1" +version = "0.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" dependencies = [ "bitflags 1.3.2", + "fluent-uri", "serde", "serde_json", "serde_repr", - "url", ] [[package]] @@ -1920,12 +1765,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - [[package]] name = "phf" version = "0.11.3" @@ -2938,17 +2777,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "tar" version = "0.4.44" @@ -3094,16 +2922,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.8.0" @@ -3384,30 +3202,6 @@ dependencies = [ "rand", ] -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.1" @@ -3900,18 +3694,6 @@ dependencies = [ "bitflags 2.9.4", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "xattr" version = "1.5.0" @@ -3934,30 +3716,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.35" @@ -3998,49 +3756,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "zstd" version = "0.13.2" @@ -4068,3 +3783,8 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "displaydoc" +version = "0.2.5" +source = "git+https://github.com/yaahc/displaydoc?rev=7dc6e324b1788a6b7fb9f3a1953c512923a3e9f0#7dc6e324b1788a6b7fb9f3a1953c512923a3e9f0" diff --git a/crates/pyrefly_python/Cargo.toml b/crates/pyrefly_python/Cargo.toml index 2be8eb88c..f2bfd9d77 100644 --- a/crates/pyrefly_python/Cargo.toml +++ b/crates/pyrefly_python/Cargo.toml @@ -13,7 +13,7 @@ anyhow = "1.0.98" dupe = "0.9.1" equivalent = "1.0" itertools = "0.14.0" -lsp-types = "0.94.1" +lsp-types = "0.97.0" parse-display = "0.8.2" pathdiff = "0.2" pyrefly_util = { path = "../pyrefly_util" } diff --git a/crates/pyrefly_util/Cargo.toml b/crates/pyrefly_util/Cargo.toml index 1402d6e41..f5ddf5033 100644 --- a/crates/pyrefly_util/Cargo.toml +++ b/crates/pyrefly_util/Cargo.toml @@ -23,7 +23,7 @@ human_bytes = "0.4.3" ignore = "0.4" itertools = "0.14.0" lock_free_hashtable = "0.1.1" -lsp-types = "0.94.1" +lsp-types = "0.97.0" memory-stats = "1.2.0" notify = "5" parse-display = "0.8.2" diff --git a/crates/tsp_types/Cargo.toml b/crates/tsp_types/Cargo.toml index 91c52cfd5..466da7797 100644 --- a/crates/tsp_types/Cargo.toml +++ b/crates/tsp_types/Cargo.toml @@ -10,6 +10,6 @@ license = "MIT" [dependencies] lsp-server = "0.7.2" -lsp-types = "0.94.1" +lsp-types = "0.97.0" serde = { version = "1.0.219", features = ["derive", "rc"] } serde_json = { version = "1.0.140", features = ["alloc", "float_roundtrip", "raw_value", "unbounded_depth"] } diff --git a/pyrefly/Cargo.toml b/pyrefly/Cargo.toml index 7a318d562..afb2aea39 100644 --- a/pyrefly/Cargo.toml +++ b/pyrefly/Cargo.toml @@ -28,7 +28,7 @@ fuzzy-matcher = "0.3.7" indicatif = { version = "0.17.6", features = ["futures", "improved_unicode", "rayon", "tokio"] } itertools = "0.14.0" lsp-server = "0.7.2" -lsp-types = "0.94.1" +lsp-types = "0.97.0" num-traits = { version = "0.2.19", default-features = false } parse-display = "0.8.2" paste = "1.0.14" From b781cbf711322c55220e135b458bf98a71672c8c Mon Sep 17 00:00:00 2001 From: Kyle Into Date: Fri, 17 Oct 2025 12:44:24 -0700 Subject: [PATCH 2/2] .ipynb support (#1333) Summary: https://github.com/facebook/pyrefly/issues/381 Differential Revision: D84929386 --- crates/pyrefly_python/Cargo.toml | 2 +- crates/pyrefly_python/src/lib.rs | 3 +- crates/pyrefly_python/src/module_name.rs | 4 +- crates/pyrefly_python/src/module_path.rs | 1 + crates/pyrefly_python/src/notebook.rs | 134 +++++++++++++++++++++ lsp/package.json | 3 +- lsp/src/extension.ts | 20 +++- pyrefly/lib/lsp/server.rs | 142 +++++++++++++++++++---- 8 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 crates/pyrefly_python/src/notebook.rs diff --git a/crates/pyrefly_python/Cargo.toml b/crates/pyrefly_python/Cargo.toml index f2bfd9d77..92477b22c 100644 --- a/crates/pyrefly_python/Cargo.toml +++ b/crates/pyrefly_python/Cargo.toml @@ -22,10 +22,10 @@ ruff_python_ast = { git = "https://github.com/astral-sh/ruff/", rev = "9bee8376a ruff_python_parser = { git = "https://github.com/astral-sh/ruff/", rev = "9bee8376a17401f9736b45fdefffb62edc2f1668" } ruff_text_size = { git = "https://github.com/astral-sh/ruff/", rev = "9bee8376a17401f9736b45fdefffb62edc2f1668" } serde = { version = "1.0.219", features = ["derive", "rc"] } +serde_json = { version = "1.0.140", features = ["alloc", "float_roundtrip", "raw_value", "unbounded_depth"] } starlark_map = "0.13.0" static_interner = "0.1.1" thiserror = "2.0.12" [dev-dependencies] -serde_json = { version = "1.0.140", features = ["alloc", "float_roundtrip", "raw_value", "unbounded_depth"] } toml = { version = "0.9.2", features = ["preserve_order"] } diff --git a/crates/pyrefly_python/src/lib.rs b/crates/pyrefly_python/src/lib.rs index ddef3e7ba..27ffa9bf4 100644 --- a/crates/pyrefly_python/src/lib.rs +++ b/crates/pyrefly_python/src/lib.rs @@ -28,13 +28,14 @@ pub mod module; pub mod module_name; pub mod module_path; pub mod nesting_context; +pub mod notebook; pub mod qname; pub mod short_identifier; pub mod symbol_kind; pub mod sys_info; /// Suffixes of python files that we can be processed. -pub const PYTHON_EXTENSIONS: &[&str] = &["py", "pyi"]; +pub const PYTHON_EXTENSIONS: &[&str] = &["py", "pyi", "ipynb"]; /// Suffixes of compiled python modules pub const COMPILED_FILE_SUFFIXES: &[&str] = &["pyc", "pyx", "pyd"]; diff --git a/crates/pyrefly_python/src/module_name.rs b/crates/pyrefly_python/src/module_name.rs index b5927f88b..3035dc3e0 100644 --- a/crates/pyrefly_python/src/module_name.rs +++ b/crates/pyrefly_python/src/module_name.rs @@ -204,7 +204,7 @@ impl ModuleName { None => {} Some(file_name) => { let splits: Vec<&str> = file_name.rsplitn(2, '.').collect(); - if splits.len() != 2 || !(splits[0] == "py" || splits[0] == "pyi") { + if splits.len() != 2 || !(splits[0] == "py" || splits[0] == "pyi" || splits[0] == "ipynb") { return Err(anyhow::anyhow!(PathConversionError::InvalidExtension { file_name: file_name.to_owned(), })); @@ -407,8 +407,10 @@ mod tests { } assert_module_name("foo.py", "foo"); assert_module_name("foo.pyi", "foo"); + assert_module_name("foo.ipynb", "foo"); assert_module_name("foo/bar.py", "foo.bar"); assert_module_name("foo/bar.pyi", "foo.bar"); + assert_module_name("foo/bar.ipynb", "foo.bar"); assert_module_name("foo/bar/__init__.py", "foo.bar"); assert_module_name("foo/bar/__init__.pyi", "foo.bar"); diff --git a/crates/pyrefly_python/src/module_path.rs b/crates/pyrefly_python/src/module_path.rs index 03f2ba904..27493067d 100644 --- a/crates/pyrefly_python/src/module_path.rs +++ b/crates/pyrefly_python/src/module_path.rs @@ -64,6 +64,7 @@ impl ModuleStyle { if path.extension() == Some("pyi".as_ref()) { ModuleStyle::Interface } else { + // Both .py and .ipynb are executable ModuleStyle::Executable } } diff --git a/crates/pyrefly_python/src/notebook.rs b/crates/pyrefly_python/src/notebook.rs new file mode 100644 index 000000000..3a712a859 --- /dev/null +++ b/crates/pyrefly_python/src/notebook.rs @@ -0,0 +1,134 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// Jupyter notebook parsing support. +// +// This module extracts Python code from `.ipynb` files (Jupyter notebooks) +// and converts them into a single Python source file that can be analyzed +// by the type checker. + +use anyhow::Context; +use anyhow::Result; +use serde::Deserialize; + +/// Represents a Jupyter notebook cell +#[derive(Debug, Deserialize)] +struct NotebookCell { + cell_type: String, + source: NotebookSource, +} + +/// Source can be either a string or an array of strings +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum NotebookSource { + String(String), + Array(Vec), +} + +/// Minimal representation of a Jupyter notebook +#[derive(Debug, Deserialize)] +struct Notebook { + cells: Vec, +} + +/// Extracts Python code from a Jupyter notebook JSON string. +/// +/// This function: +/// - Parses the notebook JSON +/// - Extracts only code cells (ignoring markdown cells) +/// - Concatenates all code into a single Python source string +/// - Adds cell markers as comments for debugging +/// +/// # Arguments +/// * `content` - The raw JSON content of a `.ipynb` file +/// +/// # Returns +/// A single Python source string containing all code cells, or an error if parsing fails +pub fn extract_python_from_notebook(content: &str) -> Result { + let notebook: Notebook = + serde_json::from_str(content).context("Failed to parse notebook JSON")?; + + let mut python_code = String::new(); + let mut code_cell_count = 0; + + for cell in notebook.cells.iter() { + if cell.cell_type == "code" { + code_cell_count += 1; + // Add a comment marker for each cell + python_code.push_str(&format!("# Cell {}\n", code_cell_count)); + + // Extract the source code + match &cell.source { + NotebookSource::String(s) => { + python_code.push_str(s.as_str()); + } + NotebookSource::Array(lines) => { + for line in lines { + python_code.push_str(line.as_str()); + } + } + } + + // Add spacing between cells + if !python_code.ends_with('\n') { + python_code.push('\n'); + } + python_code.push('\n'); + } + } + + Ok(python_code) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_python_from_notebook() { + let notebook_json = r##"{ + "cells": [ + { + "cell_type": "code", + "source": ["def hello():\n", " return 'world'\n"] + }, + { + "cell_type": "markdown", + "source": ["This is a markdown cell"] + }, + { + "cell_type": "code", + "source": "x = 5" + } + ] + }"##; + + let result = extract_python_from_notebook(notebook_json).unwrap(); + + assert!(result.contains("# Cell 1")); + assert!(result.contains("def hello():")); + assert!(result.contains("return 'world'")); + assert!(!result.contains("This is a markdown cell")); + assert!(result.contains("# Cell 2")); + assert!(result.contains("x = 5")); + } + + #[test] + fn test_extract_python_from_empty_notebook() { + let notebook_json = r#"{"cells": []}"#; + let result = extract_python_from_notebook(notebook_json).unwrap(); + assert_eq!(result, ""); + } + + #[test] + fn test_extract_python_with_invalid_json() { + let invalid_json = "not valid json"; + let result = extract_python_from_notebook(invalid_json); + assert!(result.is_err()); + } +} diff --git a/lsp/package.json b/lsp/package.json index 7b2f9ffb1..9b53ef233 100644 --- a/lsp/package.json +++ b/lsp/package.json @@ -33,7 +33,8 @@ }, "main": "./dist/extension", "activationEvents": [ - "onLanguage:python" + "onLanguage:python", + "onNotebook:jupyter-notebook" ], "contributes": { "languages": [ diff --git a/lsp/src/extension.ts b/lsp/src/extension.ts index 30e8f11fc..c223d68b6 100644 --- a/lsp/src/extension.ts +++ b/lsp/src/extension.ts @@ -41,7 +41,8 @@ async function updateStatusBar() { const document = vscode.window.activeTextEditor?.document; if ( document == null || - document.uri.scheme !== 'file' || + (document.uri.scheme !== 'file' && + document.uri.scheme !== 'vscode-notebook-cell') || document.languageId !== 'python' ) { statusBarItem?.hide(); @@ -180,8 +181,21 @@ export async function activate(context: ExtensionContext) { // Options to control the language client let clientOptions: LanguageClientOptions = { initializationOptions: rawInitialisationOptions, - // Register the server for Starlark documents - documentSelector: [{scheme: 'file', language: 'python'}], + // Register the server for Python documents + documentSelector: [ + {scheme: 'file', language: 'python'}, + // Support for notebook cells + {scheme: 'vscode-notebook-cell', language: 'python'}, + ], + // Support for notebooks + notebookDocumentSync: { + notebookSelector: [ + { + notebook: {notebookType: 'jupyter-notebook'}, + cells: [{language: 'python'}], + }, + ], + }, outputChannel: outputChannel, middleware: { workspace: { diff --git a/pyrefly/lib/lsp/server.rs b/pyrefly/lib/lsp/server.rs index e798fb6e2..77205741a 100644 --- a/pyrefly/lib/lsp/server.rs +++ b/pyrefly/lib/lsp/server.rs @@ -67,6 +67,10 @@ use lsp_types::InlayHint; use lsp_types::InlayHintLabel; use lsp_types::InlayHintParams; use lsp_types::Location; +use lsp_types::Notebook; +use lsp_types::NotebookCellSelector; +use lsp_types::NotebookDocumentSyncOptions; +use lsp_types::NotebookSelector; use lsp_types::NumberOrString; use lsp_types::OneOf; use lsp_types::Position; @@ -149,6 +153,7 @@ use pyrefly_python::PYTHON_EXTENSIONS; use pyrefly_python::module::TextRangeWithModule; use pyrefly_python::module_name::ModuleName; use pyrefly_python::module_path::ModulePath; +use pyrefly_python::notebook::extract_python_from_notebook; use pyrefly_util::arc_id::ArcId; use pyrefly_util::events::CategorizedEvents; use pyrefly_util::globs::Globs; @@ -390,6 +395,15 @@ pub fn capabilities( text_document_sync: Some(TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, )), + notebook_document_sync: Some(OneOf::Left(NotebookDocumentSyncOptions { + notebook_selector: vec![NotebookSelector::ByNotebook { + notebook: Notebook::String("jupyter-notebook".to_owned()), + cells: Some(vec![NotebookCellSelector { + language: "python".to_owned(), + }]), + }], + save: Some(false), + })), definition_provider: Some(OneOf::Left(true)), type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions { @@ -516,6 +530,28 @@ pub fn lsp_loop( impl Server { const FILEWATCHER_ID: &str = "FILEWATCHER"; + /// Extract the notebook file path from a vscode-notebook-cell URI. + /// These URIs have the format: vscode-notebook-cell:/path/to/notebook.ipynb#W0sZmlsZQ%3D%3D + /// We extract the path before the fragment (#). + fn notebook_cell_uri_to_path(uri: &Url) -> Option { + if uri.scheme() == "vscode-notebook-cell" { + // Extract the path portion without the fragment + let path_str = uri.path(); + PathBuf::from(path_str).into() + } else { + None + } + } + + /// Convert a URI to a file path, handling both regular file:// URIs and vscode-notebook-cell:// URIs + fn uri_to_file_path(uri: &Url) -> Result { + if uri.scheme() == "vscode-notebook-cell" { + Self::notebook_cell_uri_to_path(uri).ok_or(()) + } else { + uri.to_file_path() + } + } + fn extract_request_params_or_send_err_response( &self, params: Result, @@ -887,11 +923,17 @@ impl Server { } } else if &x.method == "pyrefly/textDocument/typeErrorDisplayStatus" { let text_document: TextDocumentIdentifier = serde_json::from_value(x.params)?; + let Ok(path) = Self::uri_to_file_path(&text_document.uri) else { + self.send_response(Response::new_err( + x.id, + ErrorCode::InvalidParams as i32, + format!("Could not convert uri to filepath: {}", text_document.uri), + )); + return Ok(ProcessEvent::Continue); + }; self.send_response(new_response( x.id, - Ok(self.type_error_display_status( - text_document.uri.to_file_path().unwrap().as_path(), - )), + Ok(self.type_error_display_status(path.as_path())), )); } else { self.send_response(Response::new_err( @@ -919,7 +961,7 @@ impl Server { { folders .iter() - .map(|x| x.uri.to_file_path().unwrap()) + .filter_map(|x| Self::uri_to_file_path(&x.uri).ok()) .collect() } else { Vec::new() @@ -1289,7 +1331,13 @@ impl Server { } fn did_save(&self, params: DidSaveTextDocumentParams) { - let file = params.text_document.uri.to_file_path().unwrap(); + let Ok(file) = Self::uri_to_file_path(¶ms.text_document.uri) else { + eprintln!( + "Could not convert uri to filepath: {}", + params.text_document.uri + ); + return; + }; self.invalidate(move |t| t.invalidate_disk(&[file])); } @@ -1299,7 +1347,7 @@ impl Server { subsequent_mutation: bool, params: DidOpenTextDocumentParams, ) -> anyhow::Result<()> { - let uri = params.text_document.uri.to_file_path().map_err(|_| { + let uri = Self::uri_to_file_path(¶ms.text_document.uri).map_err(|_| { anyhow::anyhow!( "Could not convert uri to filepath: {}", params.text_document.uri @@ -1315,9 +1363,25 @@ impl Server { self.version_info .lock() .insert(uri.clone(), params.text_document.version); - self.open_files - .write() - .insert(uri, Arc::new(params.text_document.text)); + + // Handle .ipynb files by extracting Python code + // Skip extraction for notebook cells (vscode-notebook-cell:// URIs) as their content is already Python + let text_content = if uri.extension() == Some("ipynb".as_ref()) + && params.text_document.uri.scheme() != "vscode-notebook-cell" + { + match extract_python_from_notebook(¶ms.text_document.text) { + Ok(python_code) => python_code, + Err(err) => { + eprintln!("Failed to parse notebook {}: {}", uri.display(), err); + // Fall back to empty string if parsing fails + String::new() + } + } + } else { + params.text_document.text + }; + + self.open_files.write().insert(uri, Arc::new(text_content)); if !subsequent_mutation { self.validate_in_memory(ide_transaction_manager); } @@ -1335,7 +1399,8 @@ impl Server { params: DidChangeTextDocumentParams, ) -> anyhow::Result<()> { let VersionedTextDocumentIdentifier { uri, version } = params.text_document; - let file_path = uri.to_file_path().unwrap(); + let file_path = Self::uri_to_file_path(&uri) + .map_err(|_| anyhow::anyhow!("Could not convert uri to filepath: {}", uri))?; let mut version_info = self.version_info.lock(); let old_version = version_info.get(&file_path).unwrap_or(&0); @@ -1347,10 +1412,26 @@ impl Server { version_info.insert(file_path.clone(), version); let mut lock = self.open_files.write(); let original = lock.get_mut(&file_path).unwrap(); - *original = Arc::new(apply_change_events( - original.as_str(), - params.content_changes, - )); + let updated_content = apply_change_events(original.as_str(), params.content_changes); + + // Handle .ipynb files by extracting Python code + // Skip extraction for notebook cells (vscode-notebook-cell:// URIs) as their content is already Python + let text_content = if file_path.extension() == Some("ipynb".as_ref()) + && uri.scheme() != "vscode-notebook-cell" + { + match extract_python_from_notebook(&updated_content) { + Ok(python_code) => python_code, + Err(err) => { + eprintln!("Failed to parse notebook {}: {}", file_path.display(), err); + // Fall back to empty string if parsing fails + String::new() + } + } + } else { + updated_content + }; + + *original = Arc::new(text_content); drop(lock); if !subsequent_mutation { self.validate_in_memory(ide_transaction_manager); @@ -1392,7 +1473,13 @@ impl Server { } fn did_close(&self, params: DidCloseTextDocumentParams) { - let uri = params.text_document.uri.to_file_path().unwrap(); + let Ok(uri) = Self::uri_to_file_path(¶ms.text_document.uri) else { + eprintln!( + "Could not convert uri to filepath: {}", + params.text_document.uri + ); + return; + }; self.version_info.lock().remove(&uri); self.open_files.write().remove(&uri); self.connection @@ -1472,7 +1559,7 @@ impl Server { &self, uri: &Url, ) -> Option<(Handle, Option)> { - let path = uri.to_file_path().unwrap(); + let path = Self::uri_to_file_path(uri).ok()?; self.workspaces.get_with(path.clone(), |workspace| { if workspace.disable_language_services { eprintln!("Skipping request - language services disabled"); @@ -1896,11 +1983,10 @@ impl Server { params: DocumentSymbolParams, ) -> Option> { let uri = ¶ms.text_document.uri; + let path = Self::uri_to_file_path(uri).ok()?; if self .workspaces - .get_with(uri.to_file_path().unwrap(), |workspace| { - workspace.disable_language_services - }) + .get_with(path, |workspace| workspace.disable_language_services) || !self .initialize_params .capabilities @@ -1944,10 +2030,20 @@ impl Server { transaction: &Transaction<'_>, params: DocumentDiagnosticParams, ) -> DocumentDiagnosticReport { - let handle = make_open_handle( - &self.state, - ¶ms.text_document.uri.to_file_path().unwrap(), - ); + let path = match Self::uri_to_file_path(¶ms.text_document.uri) { + Ok(path) => path, + Err(_) => { + // If we can't convert the URI to a path, return empty diagnostics + return DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { + full_document_diagnostic_report: FullDocumentDiagnosticReport { + items: Vec::new(), + result_id: None, + }, + related_documents: None, + }); + } + }; + let handle = make_open_handle(&self.state, &path); let mut items = Vec::new(); let open_files = &self.open_files.read(); for e in transaction.get_errors(once(&handle)).collect_errors().shown {