Overview
The Glass Extension API provides comprehensive support for integrating Language Server Protocol (LSP) servers. LSP enables:
Code completion and IntelliSense
Go to definition and find references
Diagnostics (errors and warnings)
Code actions and refactoring
Hover information and documentation
Semantic tokens and syntax highlighting
Glass uses the standard LSP specification. Any language server implementing the protocol can be integrated.
Language Server Lifecycle
Starting a Language Server
Implement language_server_command to tell Glass how to start your language server:
use zed_extension_api :: { self as zed, LanguageServerId , Result };
impl zed :: Extension for MyExtension {
fn language_server_command (
& mut self ,
language_server_id : & LanguageServerId ,
worktree : & zed :: Worktree ,
) -> Result < zed :: Command > {
Ok ( zed :: Command {
command : "/path/to/language-server" . to_string (),
args : vec! [ "--stdio" . to_string ()],
env : vec! [
( "LOG_LEVEL" . to_string (), "info" . to_string ()),
],
})
}
}
Path to the language server executable. Can be absolute or relative.
Command-line arguments passed to the language server. Most LSP servers use --stdio for standard input/output communication.
Environment variables to set for the language server process.
Server Installation Status
Provide feedback during language server installation:
use zed :: LanguageServerInstallationStatus ;
fn install_server (
language_server_id : & LanguageServerId ,
) -> Result < String > {
zed :: set_language_server_installation_status (
language_server_id ,
& LanguageServerInstallationStatus :: CheckingForUpdate ,
);
let version = check_latest_version () ? ;
zed :: set_language_server_installation_status (
language_server_id ,
& LanguageServerInstallationStatus :: Downloading ,
);
download_server ( & version ) ? ;
// Return path to installed server
Ok ( format! ( "servers/my-server-{}" , version ))
}
Status values:
None - No active installation
CheckingForUpdate - Checking for newer versions
Downloading - Downloading the server
Failed(String) - Installation failed with error message
Configuration
Initialization Options
Sent to the language server during initialization:
impl zed :: Extension for MyExtension {
fn language_server_initialization_options (
& mut self ,
_server_id : & LanguageServerId ,
_worktree : & zed :: Worktree ,
) -> Result < Option < zed :: serde_json :: Value >> {
Ok ( Some ( zed :: serde_json :: json! ({
"trace" : "messages" ,
"logLevel" : "debug" ,
"preferences" : {
"includeInlayHints" : true
}
})))
}
}
Initialization options are sent once when the language server starts. Use them for settings that don’t change during the session.
Workspace Configuration
Sent to the language server in response to workspace/configuration requests:
impl zed :: Extension for MyExtension {
fn language_server_workspace_configuration (
& mut self ,
server_id : & LanguageServerId ,
worktree : & zed :: Worktree ,
) -> Result < Option < zed :: serde_json :: Value >> {
// Read user settings
let settings = zed :: settings :: LspSettings :: for_worktree (
server_id . as_ref (),
worktree ,
) ? ;
Ok ( settings . settings)
}
}
Users can configure workspace settings in their Glass settings:
{
"lsp" : {
"rust-analyzer" : {
"initialization_options" : {
"checkOnSave" : {
"command" : "clippy"
}
},
"settings" : {
"rust-analyzer.cargo.features" : "all"
}
}
}
}
Multi-Server Configuration
For extensions supporting multiple language servers, provide configuration for additional servers:
impl zed :: Extension for MyExtension {
fn language_server_additional_initialization_options (
& mut self ,
_language_server_id : & LanguageServerId ,
target_language_server_id : & LanguageServerId ,
_worktree : & zed :: Worktree ,
) -> Result < Option < zed :: serde_json :: Value >> {
match target_language_server_id . as_ref () {
"typescript" => Ok ( Some ( zed :: serde_json :: json! ({
"preferences" : {
"includeInlayParameterNameHints" : "all"
}
}))),
"eslint" => Ok ( Some ( zed :: serde_json :: json! ({
"run" : "onType" ,
"nodePath" : "./node_modules"
}))),
_ => Ok ( None ),
}
}
}
LSP Data Types
The Extension API re-exports common LSP types:
Completion
use zed :: lsp :: Completion ;
pub struct Completion {
pub label : String ,
pub kind : Option < CompletionKind >,
pub detail : Option < String >,
pub documentation : Option < String >,
// ... other LSP fields
}
The text displayed in the completion menu
The type of completion: Function, Variable, Class, etc.
Additional detail, like function signature
Documentation shown in completion tooltip
Symbol
use zed :: lsp :: { Symbol , SymbolKind };
pub struct Symbol {
pub name : String ,
pub kind : SymbolKind ,
pub range : Range ,
// ... other fields
}
Symbol name as it appears in code
Type of symbol: Function, Class, Variable, etc.
Location of the symbol in the file
Symbol and Completion Kinds
use zed :: lsp :: { CompletionKind , SymbolKind };
// Completion kinds
CompletionKind :: Function
CompletionKind :: Method
CompletionKind :: Variable
CompletionKind :: Class
CompletionKind :: Interface
CompletionKind :: Module
CompletionKind :: Property
CompletionKind :: Keyword
// ... and more
// Symbol kinds
SymbolKind :: Function
SymbolKind :: Method
SymbolKind :: Variable
SymbolKind :: Class
SymbolKind :: Interface
SymbolKind :: Namespace
SymbolKind :: Struct
SymbolKind :: Enum
// ... and more
Custom Labels
Customize how completions and symbols are displayed:
Completion Labels
use zed :: { CodeLabel , CodeLabelSpan , Range };
impl zed :: Extension for MyExtension {
fn label_for_completion (
& self ,
_language_server_id : & LanguageServerId ,
completion : zed :: lsp :: Completion ,
) -> Option < CodeLabel > {
// For functions, show with syntax highlighting
if completion . kind == Some ( zed :: lsp :: CompletionKind :: Function ) {
Some ( CodeLabel {
code : format! ( "fn {}()" , completion . label),
spans : vec! [
CodeLabelSpan :: literal ( "fn " , Some ( "keyword" . to_string ())),
CodeLabelSpan :: code_range ( Range { start : 3 , end : 3 + completion . label . len () as u32 }),
CodeLabelSpan :: literal ( "()" , Some ( "punctuation" . to_string ())),
],
filter_range : Range { start : 3 , end : 3 + completion . label . len () as u32 },
})
} else {
None // Use default rendering
}
}
}
Source code that will be parsed by Tree-sitter for syntax highlighting
spans
Vec<CodeLabelSpan>
required
Defines which parts of the code to display and how to highlight them
The range of text used when filtering/matching in the completion list
Symbol Labels
Similarly, customize symbol display:
impl zed :: Extension for MyExtension {
fn label_for_symbol (
& self ,
_language_server_id : & LanguageServerId ,
symbol : zed :: lsp :: Symbol ,
) -> Option < CodeLabel > {
match symbol . kind {
zed :: lsp :: SymbolKind :: Function => {
Some ( CodeLabel {
code : format! ( "fn {}" , symbol . name),
spans : vec! [
CodeLabelSpan :: literal ( "fn " , Some ( "keyword" . to_string ())),
CodeLabelSpan :: code_range ( Range {
start : 3 ,
end : ( 3 + symbol . name . len ()) as u32 ,
}),
],
filter_range : Range {
start : 3 ,
end : ( 3 + symbol . name . len ()) as u32 ,
},
})
}
_ => None ,
}
}
}
Finding Server Binaries
Check PATH
Look for a language server already installed on the system:
fn language_server_command (
& mut self ,
_language_server_id : & LanguageServerId ,
worktree : & zed :: Worktree ,
) -> Result < zed :: Command > {
if let Some ( path ) = worktree . which ( "rust-analyzer" ) {
return Ok ( zed :: Command {
command : path ,
args : vec! [],
env : Default :: default (),
});
}
// Fallback: download and install
self . install_server ()
}
Always check worktree.which() first to respect user-installed language servers.
Download from GitHub
use zed :: {latest_github_release, GithubReleaseOptions , current_platform, Os , Architecture };
fn download_server () -> Result < String > {
let release = latest_github_release (
"rust-lang/rust-analyzer" ,
GithubReleaseOptions {
require_assets : true ,
pre_release : false ,
},
) ? ;
let platform = current_platform ();
let asset_name = match ( platform . os, platform . arch) {
( Os :: Mac , Architecture :: Aarch64 ) => "rust-analyzer-aarch64-apple-darwin.gz" ,
( Os :: Mac , Architecture :: X8664 ) => "rust-analyzer-x86_64-apple-darwin.gz" ,
( Os :: Linux , Architecture :: X8664 ) => "rust-analyzer-x86_64-unknown-linux-gnu.gz" ,
_ => return Err ( "Unsupported platform" . to_string ()),
};
let asset = release . assets
. iter ()
. find ( | a | a . name == asset_name )
. ok_or ( "Asset not found" ) ? ;
let server_path = format! ( "rust-analyzer-{}" , release . version);
zed :: download_file (
& asset . download_url,
& server_path ,
zed :: DownloadedFileType :: Gzip ,
) ? ;
zed :: make_file_executable ( & server_path ) ? ;
Ok ( server_path )
}
Install via npm
fn install_node_server (
language_server_id : & LanguageServerId ,
) -> Result < String > {
const PACKAGE : & str = "typescript-language-server" ;
const SERVER_PATH : & str = "node_modules/.bin/typescript-language-server" ;
zed :: set_language_server_installation_status (
language_server_id ,
& zed :: LanguageServerInstallationStatus :: CheckingForUpdate ,
);
let version = zed :: npm_package_latest_version ( PACKAGE ) ? ;
// Check if already installed
if let Some ( installed ) = zed :: npm_package_installed_version ( PACKAGE ) ? {
if installed == version {
return Ok ( SERVER_PATH . to_string ());
}
}
zed :: set_language_server_installation_status (
language_server_id ,
& zed :: LanguageServerInstallationStatus :: Downloading ,
);
zed :: npm_install_package ( PACKAGE , & version ) ? ;
Ok ( SERVER_PATH . to_string ())
}
Complete Example: Rust Analyzer
Here’s a complete example integrating rust-analyzer:
use zed_extension_api :: { self as zed, LanguageServerId , Result };
use std :: fs;
struct RustExtension {
cached_server_path : Option < String >,
}
impl zed :: Extension for RustExtension {
fn new () -> Self {
Self {
cached_server_path : None ,
}
}
fn language_server_command (
& mut self ,
language_server_id : & LanguageServerId ,
worktree : & zed :: Worktree ,
) -> Result < zed :: Command > {
// Check if rust-analyzer is in PATH
if let Some ( path ) = worktree . which ( "rust-analyzer" ) {
return Ok ( zed :: Command {
command : path ,
args : vec! [],
env : Default :: default (),
});
}
// Use cached path if available
if let Some ( path ) = & self . cached_server_path {
if fs :: metadata ( path ) . is_ok () {
return Ok ( zed :: Command {
command : path . clone (),
args : vec! [],
env : Default :: default (),
});
}
}
// Download from GitHub
let path = self . download_server ( language_server_id ) ? ;
self . cached_server_path = Some ( path . clone ());
Ok ( zed :: Command {
command : path ,
args : vec! [],
env : Default :: default (),
})
}
fn language_server_initialization_options (
& mut self ,
_server_id : & LanguageServerId ,
_worktree : & zed :: Worktree ,
) -> Result < Option < zed :: serde_json :: Value >> {
Ok ( Some ( zed :: serde_json :: json! ({
"checkOnSave" : {
"command" : "clippy"
},
"inlayHints" : {
"parameterHints" : { "enable" : true },
"typeHints" : { "enable" : true }
}
})))
}
fn language_server_workspace_configuration (
& mut self ,
server_id : & LanguageServerId ,
worktree : & zed :: Worktree ,
) -> Result < Option < zed :: serde_json :: Value >> {
let settings = zed :: settings :: LspSettings :: for_worktree (
server_id . as_ref (),
worktree ,
) ? ;
Ok ( settings . settings)
}
}
impl RustExtension {
fn download_server (
& self ,
language_server_id : & LanguageServerId ,
) -> Result < String > {
use zed :: {current_platform, latest_github_release, GithubReleaseOptions , Os , Architecture };
zed :: set_language_server_installation_status (
language_server_id ,
& zed :: LanguageServerInstallationStatus :: CheckingForUpdate ,
);
let release = latest_github_release (
"rust-lang/rust-analyzer" ,
GithubReleaseOptions {
require_assets : true ,
pre_release : false ,
},
) ? ;
let platform = current_platform ();
let asset_name = match ( platform . os, platform . arch) {
( Os :: Mac , Architecture :: Aarch64 ) => "rust-analyzer-aarch64-apple-darwin.gz" ,
( Os :: Mac , Architecture :: X8664 ) => "rust-analyzer-x86_64-apple-darwin.gz" ,
( Os :: Linux , Architecture :: X8664 ) => "rust-analyzer-x86_64-unknown-linux-gnu.gz" ,
( Os :: Windows , Architecture :: X8664 ) => "rust-analyzer-x86_64-pc-windows-msvc.gz" ,
_ => return Err ( "Unsupported platform" . to_string ()),
};
let asset = release
. assets
. iter ()
. find ( | a | a . name == asset_name )
. ok_or_else ( || format! ( "No asset found for {}" , asset_name )) ? ;
zed :: set_language_server_installation_status (
language_server_id ,
& zed :: LanguageServerInstallationStatus :: Downloading ,
);
let server_path = format! ( "rust-analyzer-{}" , release . version);
zed :: download_file (
& asset . download_url,
& server_path ,
zed :: DownloadedFileType :: Gzip ,
) ? ;
zed :: make_file_executable ( & server_path ) ? ;
Ok ( server_path )
}
}
zed :: register_extension! ( RustExtension );
Best Practices
Respect user installations : Always check worktree.which() before downloading your own copy.
Cache server paths : Store the path after first installation to avoid repeated lookups.
Provide installation feedback : Use set_language_server_installation_status to show progress in the UI.
Don’t panic on errors : Return Result types and let Glass handle error reporting gracefully.
Support user configuration : Always check for user-provided LSP settings and merge them with defaults.
Troubleshooting
Server Won’t Start
Check that the command path is correct and the file exists
Verify the server binary has execute permissions (make_file_executable)
Ensure --stdio or equivalent flag is passed for standard I/O communication
Check environment variables are set correctly
Configuration Not Applied
Verify you’re implementing both initialization_options and workspace_configuration
Check the JSON structure matches what the language server expects
Look at the language server’s documentation for correct setting names
Completions Not Showing
Most completions work automatically with LSP. If you need custom labels:
Implement label_for_completion to customize display
Ensure the filter_range includes the text users will type
Use appropriate highlight names that match your language’s Tree-sitter grammar
Next Steps
Commands API Learn about executing commands and managing processes
Configuration Access user settings and extension configuration