python: Uplift basedpyright support into core (#35250)
This PR adds a built-in adapter for the basedpyright language server. For now, it's behind the `basedpyright` feature flag, and needs to be requested explicitly like this for staff: ``` "languages": { "Python": { "language_servers": ["basedpyright", "!pylsp", "!pyright"] } } ``` (After uninstalling the basedpyright extension.) Release Notes: - N/A
This commit is contained in:
parent
e5269212ad
commit
cfd5b8ff10
4 changed files with 362 additions and 1 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9226,6 +9226,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"collections",
|
||||
"dap",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
|
|
|
@ -41,6 +41,7 @@ async-trait.workspace = true
|
|||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Context as _;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
|
||||
use gpui::{App, UpdateGlobal};
|
||||
use node_runtime::NodeRuntime;
|
||||
use python::PyprojectTomlManifestProvider;
|
||||
|
@ -11,7 +12,7 @@ use util::{ResultExt, asset_str};
|
|||
|
||||
pub use language::*;
|
||||
|
||||
use crate::json::JsonTaskProvider;
|
||||
use crate::{json::JsonTaskProvider, python::BasedPyrightLspAdapter};
|
||||
|
||||
mod bash;
|
||||
mod c;
|
||||
|
@ -52,6 +53,12 @@ pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock<Arc<Language>> =
|
|||
))
|
||||
});
|
||||
|
||||
struct BasedPyrightFeatureFlag;
|
||||
|
||||
impl FeatureFlag for BasedPyrightFeatureFlag {
|
||||
const NAME: &'static str = "basedpyright";
|
||||
}
|
||||
|
||||
pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
||||
#[cfg(feature = "load-grammars")]
|
||||
languages.register_native_grammars([
|
||||
|
@ -88,6 +95,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
|||
let py_lsp_adapter = Arc::new(python::PyLspAdapter::new());
|
||||
let python_context_provider = Arc::new(python::PythonContextProvider);
|
||||
let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone()));
|
||||
let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new());
|
||||
let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::default());
|
||||
let rust_context_provider = Arc::new(rust::RustContextProvider);
|
||||
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
|
||||
|
@ -228,6 +236,20 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
|
|||
);
|
||||
}
|
||||
|
||||
let mut basedpyright_lsp_adapter = Some(basedpyright_lsp_adapter);
|
||||
cx.observe_flag::<BasedPyrightFeatureFlag, _>({
|
||||
let languages = languages.clone();
|
||||
move |enabled, _| {
|
||||
if enabled {
|
||||
if let Some(adapter) = basedpyright_lsp_adapter.take() {
|
||||
languages
|
||||
.register_available_lsp_adapter(adapter.name(), move || adapter.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Register globally available language servers.
|
||||
//
|
||||
// This will allow users to add support for a built-in language server (e.g., Tailwind)
|
||||
|
|
|
@ -1290,6 +1290,343 @@ impl LspAdapter for PyLspAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BasedPyrightLspAdapter {
|
||||
python_venv_base: OnceCell<Result<Arc<Path>, String>>,
|
||||
}
|
||||
|
||||
impl BasedPyrightLspAdapter {
|
||||
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
|
||||
const BINARY_NAME: &'static str = "basedpyright-langserver";
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
python_venv_base: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
|
||||
let python_path = Self::find_base_python(delegate)
|
||||
.await
|
||||
.context("Could not find Python installation for basedpyright")?;
|
||||
let work_dir = delegate
|
||||
.language_server_download_dir(&Self::SERVER_NAME)
|
||||
.await
|
||||
.context("Could not get working directory for basedpyright")?;
|
||||
let mut path = PathBuf::from(work_dir.as_ref());
|
||||
path.push("basedpyright-venv");
|
||||
if !path.exists() {
|
||||
util::command::new_smol_command(python_path)
|
||||
.arg("-m")
|
||||
.arg("venv")
|
||||
.arg("basedpyright-venv")
|
||||
.current_dir(work_dir)
|
||||
.spawn()?
|
||||
.output()
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(path.into())
|
||||
}
|
||||
|
||||
// Find "baseline", user python version from which we'll create our own venv.
|
||||
async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
|
||||
for path in ["python3", "python"] {
|
||||
if let Some(path) = delegate.which(path.as_ref()).await {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
|
||||
self.python_venv_base
|
||||
.get_or_init(move || async move {
|
||||
Self::ensure_venv(delegate)
|
||||
.await
|
||||
.map_err(|e| format!("{e}"))
|
||||
})
|
||||
.await
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for BasedPyrightLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
Self::SERVER_NAME.clone()
|
||||
}
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<Value>> {
|
||||
// Provide minimal initialization options
|
||||
// Virtual environment configuration will be handled through workspace configuration
|
||||
Ok(Some(json!({
|
||||
"python": {
|
||||
"analysis": {
|
||||
"autoSearchPaths": true,
|
||||
"useLibraryCodeForTypes": true,
|
||||
"autoImportCompletions": true
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
async fn check_if_user_installed(
|
||||
&self,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &AsyncApp,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await {
|
||||
let env = delegate.shell_env().await;
|
||||
Some(LanguageServerBinary {
|
||||
path: bin,
|
||||
env: Some(env),
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
} else {
|
||||
let venv = toolchains
|
||||
.active_toolchain(
|
||||
delegate.worktree_id(),
|
||||
Arc::from("".as_ref()),
|
||||
LanguageName::new("Python"),
|
||||
&mut cx.clone(),
|
||||
)
|
||||
.await?;
|
||||
let path = Path::new(venv.path.as_ref())
|
||||
.parent()?
|
||||
.join(Self::BINARY_NAME);
|
||||
path.exists().then(|| LanguageServerBinary {
|
||||
path,
|
||||
arguments: vec!["--stdio".into()],
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(()) as Box<_>)
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
_latest_version: Box<dyn 'static + Send + Any>,
|
||||
_container_dir: PathBuf,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
|
||||
let pip_path = venv.join(BINARY_DIR).join("pip3");
|
||||
ensure!(
|
||||
util::command::new_smol_command(pip_path.as_path())
|
||||
.arg("install")
|
||||
.arg("basedpyright")
|
||||
.arg("-U")
|
||||
.output()
|
||||
.await?
|
||||
.status
|
||||
.success(),
|
||||
"basedpyright installation failed"
|
||||
);
|
||||
let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
|
||||
Ok(LanguageServerBinary {
|
||||
path: pylsp,
|
||||
env: None,
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
_container_dir: PathBuf,
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let venv = self.base_venv(delegate).await.ok()?;
|
||||
let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
|
||||
Some(LanguageServerBinary {
|
||||
path: pylsp,
|
||||
env: None,
|
||||
arguments: vec!["--stdio".into()],
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
|
||||
// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
|
||||
// Where `XX` is the sorting category, `YYYY` is based on most recent usage,
|
||||
// and `name` is the symbol name itself.
|
||||
//
|
||||
// Because the symbol name is included, there generally are not ties when
|
||||
// sorting by the `sortText`, so the symbol's fuzzy match score is not taken
|
||||
// into account. Here, we remove the symbol name from the sortText in order
|
||||
// to allow our own fuzzy score to be used to break ties.
|
||||
//
|
||||
// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
|
||||
for item in items {
|
||||
let Some(sort_text) = &mut item.sort_text else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = sort_text.split('.');
|
||||
let Some(first) = parts.next() else { continue };
|
||||
let Some(second) = parts.next() else { continue };
|
||||
let Some(_) = parts.next() else { continue };
|
||||
sort_text.replace_range(first.len() + second.len() + 1.., "");
|
||||
}
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let label = &item.label;
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
|
||||
lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
|
||||
lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
|
||||
lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
|
||||
_ => return None,
|
||||
};
|
||||
let filter_range = item
|
||||
.filter_text
|
||||
.as_deref()
|
||||
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
|
||||
.unwrap_or(0..label.len());
|
||||
Some(language::CodeLabel {
|
||||
text: label.clone(),
|
||||
runs: vec![(0..label.len(), highlight_id)],
|
||||
filter_range,
|
||||
})
|
||||
}
|
||||
|
||||
async fn label_for_symbol(
|
||||
&self,
|
||||
name: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let (text, filter_range, display_range) = match kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
let text = format!("def {}():\n", name);
|
||||
let filter_range = 4..4 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CLASS => {
|
||||
let text = format!("class {}:", name);
|
||||
let filter_range = 6..6 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CONSTANT => {
|
||||
let text = format!("{} = 0", name);
|
||||
let filter_range = 0..name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(language::CodeLabel {
|
||||
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
|
||||
text: text[display_range].to_string(),
|
||||
filter_range,
|
||||
})
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &dyn Fs,
|
||||
adapter: &Arc<dyn LspAdapterDelegate>,
|
||||
toolchains: Arc<dyn LanguageToolchainStore>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Value> {
|
||||
let toolchain = toolchains
|
||||
.active_toolchain(
|
||||
adapter.worktree_id(),
|
||||
Arc::from("".as_ref()),
|
||||
LanguageName::new("Python"),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.update(move |cx| {
|
||||
let mut user_settings =
|
||||
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
|
||||
.and_then(|s| s.settings.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// If we have a detected toolchain, configure Pyright to use it
|
||||
if let Some(toolchain) = toolchain {
|
||||
if user_settings.is_null() {
|
||||
user_settings = Value::Object(serde_json::Map::default());
|
||||
}
|
||||
let object = user_settings.as_object_mut().unwrap();
|
||||
|
||||
let interpreter_path = toolchain.path.to_string();
|
||||
|
||||
// Detect if this is a virtual environment
|
||||
if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() {
|
||||
if let Some(venv_dir) = interpreter_dir.parent() {
|
||||
// Check if this looks like a virtual environment
|
||||
if venv_dir.join("pyvenv.cfg").exists()
|
||||
|| venv_dir.join("bin/activate").exists()
|
||||
|| venv_dir.join("Scripts/activate.bat").exists()
|
||||
{
|
||||
// Set venvPath and venv at the root level
|
||||
// This matches the format of a pyrightconfig.json file
|
||||
if let Some(parent) = venv_dir.parent() {
|
||||
// Use relative path if the venv is inside the workspace
|
||||
let venv_path = if parent == adapter.worktree_root_path() {
|
||||
".".to_string()
|
||||
} else {
|
||||
parent.to_string_lossy().into_owned()
|
||||
};
|
||||
object.insert("venvPath".to_string(), Value::String(venv_path));
|
||||
}
|
||||
|
||||
if let Some(venv_name) = venv_dir.file_name() {
|
||||
object.insert(
|
||||
"venv".to_owned(),
|
||||
Value::String(venv_name.to_string_lossy().into_owned()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always set the python interpreter path
|
||||
// Get or create the python section
|
||||
let python = object
|
||||
.entry("python")
|
||||
.or_insert(Value::Object(serde_json::Map::default()))
|
||||
.as_object_mut()
|
||||
.unwrap();
|
||||
|
||||
// Set both pythonPath and defaultInterpreterPath for compatibility
|
||||
python.insert(
|
||||
"pythonPath".to_owned(),
|
||||
Value::String(interpreter_path.clone()),
|
||||
);
|
||||
python.insert(
|
||||
"defaultInterpreterPath".to_owned(),
|
||||
Value::String(interpreter_path),
|
||||
);
|
||||
}
|
||||
|
||||
user_settings
|
||||
})
|
||||
}
|
||||
|
||||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue