Add label_for_completion to extension API (#10175)

This PR adds the ability for extensions to implement
`label_for_completion` to customize completions coming back from the
language server.

We've used the Gleam extension as a motivating example, adding
`label_for_completion` support to it.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Marshall Bowers 2024-04-04 13:56:04 -04:00 committed by GitHub
parent 0f1c2e6f2b
commit d306b531c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1124 additions and 252 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "zed_extension_api"
version = "0.0.5"
version = "0.0.6"
description = "APIs for creating Zed extensions in Rust"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"

View file

@ -1,24 +1,69 @@
pub use wit::*;
use core::fmt;
use wit::*;
// WIT re-exports.
//
// We explicitly enumerate the symbols we want to re-export, as there are some
// that we may want to shadow to provide a cleaner Rust API.
pub use wit::{
current_platform, download_file, latest_github_release, make_file_executable, node_binary_path,
npm_install_package, npm_package_installed_version, npm_package_latest_version,
zed::extension::lsp, Architecture, CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command,
DownloadedFileType, EnvVars, GithubRelease, GithubReleaseAsset, GithubReleaseOptions,
LanguageServerInstallationStatus, Os, Range, Worktree,
};
// Undocumented WIT re-exports.
//
// These are symbols that need to be public for the purposes of implementing
// the extension host, but aren't relevant to extension authors.
#[doc(hidden)]
pub use wit::Guest;
/// A result returned from a Zed extension.
pub type Result<T, E = String> = core::result::Result<T, E>;
/// Updates the installation status for the given language server.
pub fn set_language_server_installation_status(
language_server_id: &LanguageServerId,
status: &LanguageServerInstallationStatus,
) {
wit::set_language_server_installation_status(&language_server_id.0, status)
}
/// A Zed extension.
pub trait Extension: Send + Sync {
/// Returns a new instance of the extension.
fn new() -> Self
where
Self: Sized;
/// Returns the command used to start the language server for the specified
/// language.
fn language_server_command(
&mut self,
config: LanguageServerConfig,
language_server_id: &LanguageServerId,
worktree: &Worktree,
) -> Result<Command>;
/// Returns the initialization options to pass to the specified language server.
fn language_server_initialization_options(
&mut self,
_config: LanguageServerConfig,
_language_server_id: &LanguageServerId,
_worktree: &Worktree,
) -> Result<Option<String>> {
Ok(None)
}
/// Returns the label for the given completion.
fn label_for_completion(
&self,
_language_server_id: &LanguageServerId,
_completion: Completion,
) -> Option<CodeLabel> {
None
}
}
#[macro_export]
@ -53,7 +98,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.0.4",
path: "./wit/since_v0.0.6",
});
}
@ -63,16 +108,76 @@ struct Component;
impl wit::Guest for Component {
fn language_server_command(
config: wit::LanguageServerConfig,
language_server_id: String,
worktree: &wit::Worktree,
) -> Result<wit::Command> {
extension().language_server_command(config, worktree)
let language_server_id = LanguageServerId(language_server_id);
extension().language_server_command(&language_server_id, worktree)
}
fn language_server_initialization_options(
config: LanguageServerConfig,
language_server_id: String,
worktree: &Worktree,
) -> Result<Option<String>, String> {
extension().language_server_initialization_options(config, worktree)
let language_server_id = LanguageServerId(language_server_id);
extension().language_server_initialization_options(&language_server_id, worktree)
}
fn labels_for_completions(
language_server_id: String,
completions: Vec<Completion>,
) -> Result<Vec<Option<CodeLabel>>, String> {
let language_server_id = LanguageServerId(language_server_id);
let mut labels = Vec::new();
for (ix, completion) in completions.into_iter().enumerate() {
let label = extension().label_for_completion(&language_server_id, completion);
if let Some(label) = label {
labels.resize(ix + 1, None);
*labels.last_mut().unwrap() = Some(label);
}
}
Ok(labels)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct LanguageServerId(String);
impl fmt::Display for LanguageServerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl CodeLabelSpan {
/// Returns a [`CodeLabelSpan::CodeRange`].
pub fn code_range(range: impl Into<wit::Range>) -> Self {
Self::CodeRange(range.into())
}
/// Returns a [`CodeLabelSpan::Literal`].
pub fn literal(text: impl Into<String>, highlight_name: Option<String>) -> Self {
Self::Literal(CodeLabelSpanLiteral {
text: text.into(),
highlight_name,
})
}
}
impl From<std::ops::Range<u32>> for wit::Range {
fn from(value: std::ops::Range<u32>) -> Self {
Self {
start: value.start,
end: value.end,
}
}
}
impl From<std::ops::Range<usize>> for wit::Range {
fn from(value: std::ops::Range<usize>) -> Self {
Self {
start: value.start as u32,
end: value.end as u32,
}
}
}

View file

@ -0,0 +1,120 @@
package zed:extension;
world extension {
import lsp;
use lsp.{completion};
export init-extension: func();
record github-release {
version: string,
assets: list<github-release-asset>,
}
record github-release-asset {
name: string,
download-url: string,
}
record github-release-options {
require-assets: bool,
pre-release: bool,
}
enum os {
mac,
linux,
windows,
}
enum architecture {
aarch64,
x86,
x8664,
}
enum downloaded-file-type {
gzip,
gzip-tar,
zip,
uncompressed,
}
variant language-server-installation-status {
none,
downloading,
checking-for-update,
failed(string),
}
/// Gets the current operating system and architecture
import current-platform: func() -> tuple<os, architecture>;
/// Get the path to the node binary used by Zed.
import node-binary-path: func() -> result<string, string>;
/// Gets the latest version of the given NPM package.
import npm-package-latest-version: func(package-name: string) -> result<string, string>;
/// Returns the installed version of the given NPM package, if it exists.
import npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
/// Installs the specified NPM package.
import npm-install-package: func(package-name: string, version: string) -> result<_, string>;
/// Gets the latest release for the given GitHub repository.
import latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
/// Downloads a file from the given url, and saves it to the given path within the extension's
/// working directory. Extracts the file according to the given file type.
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
/// Makes the file at the given path executable.
import make-file-executable: func(filepath: string) -> result<_, string>;
/// Updates the installation status for the given language server.
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
type env-vars = list<tuple<string, string>>;
record command {
command: string,
args: list<string>,
env: env-vars,
}
resource worktree {
read-text-file: func(path: string) -> result<string, string>;
which: func(binary-name: string) -> option<string>;
shell-env: func() -> env-vars;
}
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
record code-label {
/// The source code to parse with Tree-sitter.
code: string,
spans: list<code-label-span>,
filter-range: range,
}
variant code-label-span {
/// A range into the parsed code.
code-range(range),
literal(code-label-span-literal),
}
record code-label-span-literal {
text: string,
highlight-name: option<string>,
}
record range {
start: u32,
end: u32,
}
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
}

View file

@ -0,0 +1,44 @@
interface lsp {
/// An LSP completion.
record completion {
label: string,
detail: option<string>,
kind: option<completion-kind>,
insert-text-format: option<insert-text-format>,
}
variant completion-kind {
text,
method,
function,
%constructor,
field,
variable,
class,
%interface,
module,
property,
unit,
value,
%enum,
keyword,
snippet,
color,
file,
reference,
folder,
enum-member,
constant,
struct,
event,
operator,
type-parameter,
other(s32),
}
variant insert-text-format {
plain-text,
snippet,
other(s32),
}
}