mirror of
https://github.com/zed-industries/zed.git
synced 2025-02-06 10:42:08 +00:00
fb6cff89d7
This pull request introduces a new `InlineCompletionProvider` trait, which enables making `Editor` copilot-agnostic and lets us push all the copilot functionality into the `copilot_ui` module. Long-term, I would like to merge `copilot` and `copilot_ui`, but right now `project` depends on `copilot`, which makes this impossible. The reason for adding this new trait is so that we can experiment with other inline completion providers and swap them at runtime using config settings. Please, note also that we renamed some of the existing copilot actions to be more agnostic (see release notes below). We still kept the old actions bound for backwards-compatibility, but we should probably remove them at some later version. Also, as a drive-by, we added new methods to the `Global` trait that let you read or mutate a global directly, e.g.: ```rs MyGlobal::update(cx, |global, cx| { }); ``` Release Notes: - Renamed the `copilot::Suggest` action to `editor::ShowInlineCompletion` - Renamed the `copilot::NextSuggestion` action to `editor::NextInlineCompletion` - Renamed the `copilot::PreviousSuggestion` action to `editor::PreviousInlineCompletion` - Renamed the `editor::AcceptPartialCopilotSuggestion` action to `editor::AcceptPartialInlineCompletion` --------- Co-authored-by: Nathan <nathan@zed.dev> Co-authored-by: Kyle <kylek@zed.dev> Co-authored-by: Kyle Kelley <rgbkrk@gmail.com>
299 lines
11 KiB
Rust
299 lines
11 KiB
Rust
use anyhow::Result;
|
|
use async_trait::async_trait;
|
|
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
|
use lsp::LanguageServerBinary;
|
|
use node_runtime::NodeRuntime;
|
|
use std::{
|
|
any::Any,
|
|
ffi::OsString,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
use util::ResultExt;
|
|
|
|
const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
|
|
|
|
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
|
vec![server_path.into(), "--stdio".into()]
|
|
}
|
|
|
|
pub struct PythonLspAdapter {
|
|
node: Arc<dyn NodeRuntime>,
|
|
}
|
|
|
|
impl PythonLspAdapter {
|
|
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
|
|
PythonLspAdapter { node }
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspAdapter for PythonLspAdapter {
|
|
fn name(&self) -> LanguageServerName {
|
|
LanguageServerName("pyright".into())
|
|
}
|
|
|
|
async fn fetch_latest_server_version(
|
|
&self,
|
|
_: &dyn LspAdapterDelegate,
|
|
) -> Result<Box<dyn 'static + Any + Send>> {
|
|
Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
|
|
}
|
|
|
|
async fn fetch_server_binary(
|
|
&self,
|
|
latest_version: Box<dyn 'static + Send + Any>,
|
|
container_dir: PathBuf,
|
|
_: &dyn LspAdapterDelegate,
|
|
) -> Result<LanguageServerBinary> {
|
|
let latest_version = latest_version.downcast::<String>().unwrap();
|
|
let server_path = container_dir.join(SERVER_PATH);
|
|
let package_name = "pyright";
|
|
|
|
let should_install_language_server = self
|
|
.node
|
|
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
|
|
.await;
|
|
|
|
if should_install_language_server {
|
|
self.node
|
|
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
|
|
.await?;
|
|
}
|
|
|
|
Ok(LanguageServerBinary {
|
|
path: self.node.binary_path().await?,
|
|
env: None,
|
|
arguments: server_binary_arguments(&server_path),
|
|
})
|
|
}
|
|
|
|
async fn cached_server_binary(
|
|
&self,
|
|
container_dir: PathBuf,
|
|
_: &dyn LspAdapterDelegate,
|
|
) -> Option<LanguageServerBinary> {
|
|
get_cached_server_binary(container_dir, &*self.node).await
|
|
}
|
|
|
|
async fn installation_test_binary(
|
|
&self,
|
|
container_dir: PathBuf,
|
|
) -> Option<LanguageServerBinary> {
|
|
get_cached_server_binary(container_dir, &*self.node).await
|
|
}
|
|
|
|
async fn process_completion(&self, item: &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
|
|
let Some(sort_text) = &mut item.sort_text else {
|
|
return;
|
|
};
|
|
let mut parts = sort_text.split('.');
|
|
let Some(first) = parts.next() else { return };
|
|
let Some(second) = parts.next() else { return };
|
|
let Some(_) = parts.next() else { return };
|
|
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,
|
|
};
|
|
Some(language::CodeLabel {
|
|
text: label.clone(),
|
|
runs: vec![(0..label.len(), highlight_id)],
|
|
filter_range: 0..label.len(),
|
|
})
|
|
}
|
|
|
|
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 get_cached_server_binary(
|
|
container_dir: PathBuf,
|
|
node: &dyn NodeRuntime,
|
|
) -> Option<LanguageServerBinary> {
|
|
let server_path = container_dir.join(SERVER_PATH);
|
|
if server_path.exists() {
|
|
Some(LanguageServerBinary {
|
|
path: node.binary_path().await.log_err()?,
|
|
env: None,
|
|
arguments: server_binary_arguments(&server_path),
|
|
})
|
|
} else {
|
|
log::error!("missing executable in directory {:?}", server_path);
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
|
|
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
|
|
use settings::SettingsStore;
|
|
use std::num::NonZeroU32;
|
|
use text::BufferId;
|
|
|
|
#[gpui::test]
|
|
async fn test_python_autoindent(cx: &mut TestAppContext) {
|
|
cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
|
|
let language = crate::language("python", tree_sitter_python::language());
|
|
cx.update(|cx| {
|
|
let test_settings = SettingsStore::test(cx);
|
|
cx.set_global(test_settings);
|
|
language::init(cx);
|
|
cx.update_global::<SettingsStore, _>(|store, cx| {
|
|
store.update_user_settings::<AllLanguageSettings>(cx, |s| {
|
|
s.defaults.tab_size = NonZeroU32::new(2);
|
|
});
|
|
});
|
|
});
|
|
|
|
cx.new_model(|cx| {
|
|
let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")
|
|
.with_language(language, cx);
|
|
let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
|
|
let ix = buffer.len();
|
|
buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
|
|
};
|
|
|
|
// indent after "def():"
|
|
append(&mut buffer, "def a():\n", cx);
|
|
assert_eq!(buffer.text(), "def a():\n ");
|
|
|
|
// preserve indent after blank line
|
|
append(&mut buffer, "\n ", cx);
|
|
assert_eq!(buffer.text(), "def a():\n \n ");
|
|
|
|
// indent after "if"
|
|
append(&mut buffer, "if a:\n ", cx);
|
|
assert_eq!(buffer.text(), "def a():\n \n if a:\n ");
|
|
|
|
// preserve indent after statement
|
|
append(&mut buffer, "b()\n", cx);
|
|
assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n ");
|
|
|
|
// preserve indent after statement
|
|
append(&mut buffer, "else", cx);
|
|
assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else");
|
|
|
|
// dedent "else""
|
|
append(&mut buffer, ":", cx);
|
|
assert_eq!(buffer.text(), "def a():\n \n if a:\n b()\n else:");
|
|
|
|
// indent lines after else
|
|
append(&mut buffer, "\n", cx);
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"def a():\n \n if a:\n b()\n else:\n "
|
|
);
|
|
|
|
// indent after an open paren. the closing paren is not indented
|
|
// because there is another token before it on the same line.
|
|
append(&mut buffer, "foo(\n1)", cx);
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"def a():\n \n if a:\n b()\n else:\n foo(\n 1)"
|
|
);
|
|
|
|
// dedent the closing paren if it is shifted to the beginning of the line
|
|
let argument_ix = buffer.text().find('1').unwrap();
|
|
buffer.edit(
|
|
[(argument_ix..argument_ix + 1, "")],
|
|
Some(AutoindentMode::EachLine),
|
|
cx,
|
|
);
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"def a():\n \n if a:\n b()\n else:\n foo(\n )"
|
|
);
|
|
|
|
// preserve indent after the close paren
|
|
append(&mut buffer, "\n", cx);
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"def a():\n \n if a:\n b()\n else:\n foo(\n )\n "
|
|
);
|
|
|
|
// manually outdent the last line
|
|
let end_whitespace_ix = buffer.len() - 4;
|
|
buffer.edit(
|
|
[(end_whitespace_ix..buffer.len(), "")],
|
|
Some(AutoindentMode::EachLine),
|
|
cx,
|
|
);
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"def a():\n \n if a:\n b()\n else:\n foo(\n )\n"
|
|
);
|
|
|
|
// preserve the newly reduced indentation on the next newline
|
|
append(&mut buffer, "\n", cx);
|
|
assert_eq!(
|
|
buffer.text(),
|
|
"def a():\n \n if a:\n b()\n else:\n foo(\n )\n\n"
|
|
);
|
|
|
|
// reset to a simple if statement
|
|
buffer.edit([(0..buffer.len(), "if a:\n b(\n )")], None, cx);
|
|
|
|
// dedent "else" on the line after a closing paren
|
|
append(&mut buffer, "\n else:\n", cx);
|
|
assert_eq!(buffer.text(), "if a:\n b(\n )\nelse:\n ");
|
|
|
|
buffer
|
|
});
|
|
}
|
|
}
|