logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
[package]
name = "language_core"
version = "0.1.0"
edition = "2024"
publish = false
[lib]
path = "src/language_core.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
gpui_shared_string.workspace = true
log.workspace = true
lsp.workspace = true
parking_lot.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
tree-sitter.workspace = true
util.workspace = true
[features]
test-support = []

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,122 @@
use crate::highlight_map::HighlightId;
use std::ops::Range;
#[derive(Debug, Clone)]
pub struct Symbol {
pub name: String,
pub kind: lsp::SymbolKind,
pub container_name: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CodeLabel {
/// The text to display.
pub text: String,
/// Syntax highlighting runs.
pub runs: Vec<(Range<usize>, HighlightId)>,
/// The portion of the text that should be used in fuzzy filtering.
pub filter_range: Range<usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CodeLabelBuilder {
/// The text to display.
text: String,
/// Syntax highlighting runs.
runs: Vec<(Range<usize>, HighlightId)>,
/// The portion of the text that should be used in fuzzy filtering.
filter_range: Range<usize>,
}
impl CodeLabel {
pub fn plain(text: String, filter_text: Option<&str>) -> Self {
Self::filtered(text.clone(), text.len(), filter_text, Vec::new())
}
pub fn filtered(
text: String,
label_len: usize,
filter_text: Option<&str>,
runs: Vec<(Range<usize>, HighlightId)>,
) -> Self {
assert!(label_len <= text.len());
let filter_range = filter_text
.and_then(|filter| text.find(filter).map(|index| index..index + filter.len()))
.unwrap_or(0..label_len);
Self::new(text, filter_range, runs)
}
pub fn new(
text: String,
filter_range: Range<usize>,
runs: Vec<(Range<usize>, HighlightId)>,
) -> Self {
assert!(
text.get(filter_range.clone()).is_some(),
"invalid filter range"
);
runs.iter().for_each(|(range, _)| {
assert!(
text.get(range.clone()).is_some(),
"invalid run range with inputs. Requested range {range:?} in text '{text}'",
);
});
Self {
runs,
filter_range,
text,
}
}
pub fn text(&self) -> &str {
self.text.as_str()
}
pub fn filter_text(&self) -> &str {
&self.text[self.filter_range.clone()]
}
}
impl From<String> for CodeLabel {
fn from(value: String) -> Self {
Self::plain(value, None)
}
}
impl From<&str> for CodeLabel {
fn from(value: &str) -> Self {
Self::plain(value.to_string(), None)
}
}
impl CodeLabelBuilder {
pub fn respan_filter_range(&mut self, filter_text: Option<&str>) {
self.filter_range = filter_text
.and_then(|filter| {
self.text
.find(filter)
.map(|index| index..index + filter.len())
})
.unwrap_or(0..self.text.len());
}
pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
let start_index = self.text.len();
self.text.push_str(text);
if let Some(highlight) = highlight {
let end_index = self.text.len();
self.runs.push((start_index..end_index, highlight));
}
}
pub fn build(mut self) -> CodeLabel {
if self.filter_range.end == 0 {
self.respan_filter_range(None);
}
CodeLabel {
text: self.text,
runs: self.runs,
filter_range: self.filter_range,
}
}
}

View File

@@ -0,0 +1,76 @@
use gpui_shared_string::SharedString;
use lsp::{DiagnosticSeverity, NumberOrString};
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// A diagnostic associated with a certain range of a buffer.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
/// The name of the service that produced this diagnostic.
pub source: Option<String>,
/// The ID provided by the dynamic registration that produced this diagnostic.
pub registration_id: Option<SharedString>,
/// A machine-readable code that identifies this diagnostic.
pub code: Option<NumberOrString>,
pub code_description: Option<lsp::Uri>,
/// Whether this diagnostic is a hint, warning, or error.
pub severity: DiagnosticSeverity,
/// The human-readable message associated with this diagnostic.
pub message: String,
/// The human-readable message (in markdown format)
pub markdown: Option<String>,
/// An id that identifies the group to which this diagnostic belongs.
///
/// When a language server produces a diagnostic with
/// one or more associated diagnostics, those diagnostics are all
/// assigned a single group ID.
pub group_id: usize,
/// Whether this diagnostic is the primary diagnostic for its group.
///
/// In a given group, the primary diagnostic is the top-level diagnostic
/// returned by the language server. The non-primary diagnostics are the
/// associated diagnostics.
pub is_primary: bool,
/// Whether this diagnostic is considered to originate from an analysis of
/// files on disk, as opposed to any unsaved buffer contents. This is a
/// property of a given diagnostic source, and is configured for a given
/// language server via the `LspAdapter::disk_based_diagnostic_sources` method
/// for the language server.
pub is_disk_based: bool,
/// Whether this diagnostic marks unnecessary code.
pub is_unnecessary: bool,
/// Quick separation of diagnostics groups based by their source.
pub source_kind: DiagnosticSourceKind,
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
pub data: Option<Value>,
/// Whether to underline the corresponding text range in the editor.
pub underline: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticSourceKind {
Pulled,
Pushed,
Other,
}
impl Default for Diagnostic {
fn default() -> Self {
Self {
source: Default::default(),
source_kind: DiagnosticSourceKind::Other,
code: None,
code_description: None,
severity: DiagnosticSeverity::ERROR,
message: Default::default(),
markdown: None,
group_id: 0,
is_primary: false,
is_disk_based: false,
is_unnecessary: false,
underline: true,
data: None,
registration_id: None,
}
}
}

View File

@@ -0,0 +1,755 @@
use crate::{
HighlightId, HighlightMap, LanguageConfig, LanguageConfigOverride, LanguageName,
LanguageQueries, language_config::BracketPairConfig,
};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui_shared_string::SharedString;
use lsp::LanguageServerName;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use tree_sitter::Query;
pub static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct GrammarId(pub usize);
impl GrammarId {
pub fn new() -> Self {
Self(NEXT_GRAMMAR_ID.fetch_add(1, SeqCst))
}
}
impl Default for GrammarId {
fn default() -> Self {
Self::new()
}
}
pub struct Grammar {
id: GrammarId,
pub ts_language: tree_sitter::Language,
pub error_query: Option<Query>,
pub highlights_config: Option<HighlightsConfig>,
pub brackets_config: Option<BracketsConfig>,
pub redactions_config: Option<RedactionConfig>,
pub runnable_config: Option<RunnableConfig>,
pub indents_config: Option<IndentConfig>,
pub outline_config: Option<OutlineConfig>,
pub text_object_config: Option<TextObjectConfig>,
pub injection_config: Option<InjectionConfig>,
pub override_config: Option<OverrideConfig>,
pub debug_variables_config: Option<DebugVariablesConfig>,
pub highlight_map: Mutex<HighlightMap>,
}
pub struct HighlightsConfig {
pub query: Query,
pub identifier_capture_indices: Vec<u32>,
}
pub struct IndentConfig {
pub query: Query,
pub indent_capture_ix: u32,
pub start_capture_ix: Option<u32>,
pub end_capture_ix: Option<u32>,
pub outdent_capture_ix: Option<u32>,
pub suffixed_start_captures: HashMap<u32, SharedString>,
}
pub struct OutlineConfig {
pub query: Query,
pub item_capture_ix: u32,
pub name_capture_ix: u32,
pub context_capture_ix: Option<u32>,
pub extra_context_capture_ix: Option<u32>,
pub open_capture_ix: Option<u32>,
pub close_capture_ix: Option<u32>,
pub annotation_capture_ix: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DebuggerTextObject {
Variable,
Scope,
}
impl DebuggerTextObject {
pub fn from_capture_name(name: &str) -> Option<DebuggerTextObject> {
match name {
"debug-variable" => Some(DebuggerTextObject::Variable),
"debug-scope" => Some(DebuggerTextObject::Scope),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextObject {
InsideFunction,
AroundFunction,
InsideClass,
AroundClass,
InsideComment,
AroundComment,
}
impl TextObject {
pub fn from_capture_name(name: &str) -> Option<TextObject> {
match name {
"function.inside" => Some(TextObject::InsideFunction),
"function.around" => Some(TextObject::AroundFunction),
"class.inside" => Some(TextObject::InsideClass),
"class.around" => Some(TextObject::AroundClass),
"comment.inside" => Some(TextObject::InsideComment),
"comment.around" => Some(TextObject::AroundComment),
_ => None,
}
}
pub fn around(&self) -> Option<Self> {
match self {
TextObject::InsideFunction => Some(TextObject::AroundFunction),
TextObject::InsideClass => Some(TextObject::AroundClass),
TextObject::InsideComment => Some(TextObject::AroundComment),
_ => None,
}
}
}
pub struct TextObjectConfig {
pub query: Query,
pub text_objects_by_capture_ix: Vec<(u32, TextObject)>,
}
pub struct InjectionConfig {
pub query: Query,
pub content_capture_ix: u32,
pub language_capture_ix: Option<u32>,
pub patterns: Vec<InjectionPatternConfig>,
}
pub struct RedactionConfig {
pub query: Query,
pub redaction_capture_ix: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub enum RunnableCapture {
Named(SharedString),
Run,
}
pub struct RunnableConfig {
pub query: Query,
/// A mapping from capture index to capture kind
pub extra_captures: Vec<RunnableCapture>,
}
pub struct OverrideConfig {
pub query: Query,
pub values: HashMap<u32, OverrideEntry>,
}
#[derive(Debug)]
pub struct OverrideEntry {
pub name: String,
pub range_is_inclusive: bool,
pub value: LanguageConfigOverride,
}
#[derive(Default, Clone)]
pub struct InjectionPatternConfig {
pub language: Option<Box<str>>,
pub combined: bool,
}
#[derive(Debug)]
pub struct BracketsConfig {
pub query: Query,
pub open_capture_ix: u32,
pub close_capture_ix: u32,
pub patterns: Vec<BracketsPatternConfig>,
}
#[derive(Clone, Debug, Default)]
pub struct BracketsPatternConfig {
pub newline_only: bool,
pub rainbow_exclude: bool,
}
pub struct DebugVariablesConfig {
pub query: Query,
pub objects_by_capture_ix: Vec<(u32, DebuggerTextObject)>,
}
enum Capture<'a> {
Required(&'static str, &'a mut u32),
Optional(&'static str, &'a mut Option<u32>),
}
fn populate_capture_indices(
query: &Query,
language_name: &LanguageName,
query_type: &str,
expected_prefixes: &[&str],
captures: &mut [Capture<'_>],
) -> bool {
let mut found_required_indices = Vec::new();
'outer: for (ix, name) in query.capture_names().iter().enumerate() {
for (required_ix, capture) in captures.iter_mut().enumerate() {
match capture {
Capture::Required(capture_name, index) if capture_name == name => {
**index = ix as u32;
found_required_indices.push(required_ix);
continue 'outer;
}
Capture::Optional(capture_name, index) if capture_name == name => {
**index = Some(ix as u32);
continue 'outer;
}
_ => {}
}
}
if !name.starts_with("_")
&& !expected_prefixes
.iter()
.any(|&prefix| name.starts_with(prefix))
{
log::warn!(
"unrecognized capture name '{}' in {} {} TreeSitter query \
(suppress this warning by prefixing with '_')",
name,
language_name,
query_type
);
}
}
let mut missing_required_captures = Vec::new();
for (capture_ix, capture) in captures.iter().enumerate() {
if let Capture::Required(capture_name, _) = capture
&& !found_required_indices.contains(&capture_ix)
{
missing_required_captures.push(*capture_name);
}
}
let success = missing_required_captures.is_empty();
if !success {
log::error!(
"missing required capture(s) in {} {} TreeSitter query: {}",
language_name,
query_type,
missing_required_captures.join(", ")
);
}
success
}
impl Grammar {
pub fn new(ts_language: tree_sitter::Language) -> Self {
Self {
id: GrammarId::new(),
highlights_config: None,
brackets_config: None,
outline_config: None,
text_object_config: None,
indents_config: None,
injection_config: None,
override_config: None,
redactions_config: None,
runnable_config: None,
error_query: Query::new(&ts_language, "(ERROR) @error").ok(),
debug_variables_config: None,
ts_language,
highlight_map: Default::default(),
}
}
pub fn id(&self) -> GrammarId {
self.id
}
pub fn highlight_map(&self) -> HighlightMap {
self.highlight_map.lock().clone()
}
pub fn highlight_id_for_name(&self, name: &str) -> Option<HighlightId> {
self.highlights_config
.as_ref()?
.query
.capture_index_for_name(name)
.and_then(|capture_id| self.highlight_map.lock().get(capture_id))
}
pub fn debug_variables_config(&self) -> Option<&DebugVariablesConfig> {
self.debug_variables_config.as_ref()
}
/// Load all queries from `LanguageQueries` into this grammar, mutating the
/// associated `LanguageConfig` (the override query clears
/// `brackets.disabled_scopes_by_bracket_ix`).
pub fn with_queries(
mut self,
queries: LanguageQueries,
config: &mut LanguageConfig,
) -> Result<Self> {
let name = &config.name;
if let Some(query) = queries.highlights {
self = self
.with_highlights_query(query.as_ref())
.context("Error loading highlights query")?;
}
if let Some(query) = queries.brackets {
self = self
.with_brackets_query(query.as_ref(), name)
.context("Error loading brackets query")?;
}
if let Some(query) = queries.indents {
self = self
.with_indents_query(query.as_ref(), name)
.context("Error loading indents query")?;
}
if let Some(query) = queries.outline {
self = self
.with_outline_query(query.as_ref(), name)
.context("Error loading outline query")?;
}
if let Some(query) = queries.injections {
self = self
.with_injection_query(query.as_ref(), name)
.context("Error loading injection query")?;
}
if let Some(query) = queries.overrides {
self = self
.with_override_query(
query.as_ref(),
name,
&config.overrides,
&mut config.brackets,
&config.scope_opt_in_language_servers,
)
.context("Error loading override query")?;
}
if let Some(query) = queries.redactions {
self = self
.with_redaction_query(query.as_ref(), name)
.context("Error loading redaction query")?;
}
if let Some(query) = queries.runnables {
self = self
.with_runnable_query(query.as_ref())
.context("Error loading runnables query")?;
}
if let Some(query) = queries.text_objects {
self = self
.with_text_object_query(query.as_ref(), name)
.context("Error loading textobject query")?;
}
if let Some(query) = queries.debugger {
self = self
.with_debug_variables_query(query.as_ref(), name)
.context("Error loading debug variables query")?;
}
Ok(self)
}
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut identifier_capture_indices = Vec::new();
for name in [
"variable",
"constant",
"constructor",
"function",
"function.method",
"function.method.call",
"function.special",
"property",
"type",
"type.interface",
] {
identifier_capture_indices.extend(query.capture_index_for_name(name));
}
self.highlights_config = Some(HighlightsConfig {
query,
identifier_capture_indices,
});
Ok(self)
}
pub fn with_runnable_query(mut self, source: &str) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let extra_captures: Vec<_> = query
.capture_names()
.iter()
.map(|&name| match name {
"run" => RunnableCapture::Run,
name => RunnableCapture::Named(name.to_string().into()),
})
.collect();
self.runnable_config = Some(RunnableConfig {
extra_captures,
query,
});
Ok(self)
}
pub fn with_outline_query(
mut self,
source: &str,
language_name: &LanguageName,
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut item_capture_ix = 0;
let mut name_capture_ix = 0;
let mut context_capture_ix = None;
let mut extra_context_capture_ix = None;
let mut open_capture_ix = None;
let mut close_capture_ix = None;
let mut annotation_capture_ix = None;
if populate_capture_indices(
&query,
language_name,
"outline",
&[],
&mut [
Capture::Required("item", &mut item_capture_ix),
Capture::Required("name", &mut name_capture_ix),
Capture::Optional("context", &mut context_capture_ix),
Capture::Optional("context.extra", &mut extra_context_capture_ix),
Capture::Optional("open", &mut open_capture_ix),
Capture::Optional("close", &mut close_capture_ix),
Capture::Optional("annotation", &mut annotation_capture_ix),
],
) {
self.outline_config = Some(OutlineConfig {
query,
item_capture_ix,
name_capture_ix,
context_capture_ix,
extra_context_capture_ix,
open_capture_ix,
close_capture_ix,
annotation_capture_ix,
});
}
Ok(self)
}
pub fn with_text_object_query(
mut self,
source: &str,
language_name: &LanguageName,
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut text_objects_by_capture_ix = Vec::new();
for (ix, name) in query.capture_names().iter().enumerate() {
if let Some(text_object) = TextObject::from_capture_name(name) {
text_objects_by_capture_ix.push((ix as u32, text_object));
} else {
log::warn!(
"unrecognized capture name '{}' in {} textobjects TreeSitter query",
name,
language_name,
);
}
}
self.text_object_config = Some(TextObjectConfig {
query,
text_objects_by_capture_ix,
});
Ok(self)
}
pub fn with_debug_variables_query(
mut self,
source: &str,
language_name: &LanguageName,
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut objects_by_capture_ix = Vec::new();
for (ix, name) in query.capture_names().iter().enumerate() {
if let Some(text_object) = DebuggerTextObject::from_capture_name(name) {
objects_by_capture_ix.push((ix as u32, text_object));
} else {
log::warn!(
"unrecognized capture name '{}' in {} debugger TreeSitter query",
name,
language_name,
);
}
}
self.debug_variables_config = Some(DebugVariablesConfig {
query,
objects_by_capture_ix,
});
Ok(self)
}
pub fn with_brackets_query(
mut self,
source: &str,
language_name: &LanguageName,
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut open_capture_ix = 0;
let mut close_capture_ix = 0;
if populate_capture_indices(
&query,
language_name,
"brackets",
&[],
&mut [
Capture::Required("open", &mut open_capture_ix),
Capture::Required("close", &mut close_capture_ix),
],
) {
let patterns = (0..query.pattern_count())
.map(|ix| {
let mut config = BracketsPatternConfig::default();
for setting in query.property_settings(ix) {
let setting_key = setting.key.as_ref();
if setting_key == "newline.only" {
config.newline_only = true
}
if setting_key == "rainbow.exclude" {
config.rainbow_exclude = true
}
}
config
})
.collect();
self.brackets_config = Some(BracketsConfig {
query,
open_capture_ix,
close_capture_ix,
patterns,
});
}
Ok(self)
}
pub fn with_indents_query(
mut self,
source: &str,
language_name: &LanguageName,
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut indent_capture_ix = 0;
let mut start_capture_ix = None;
let mut end_capture_ix = None;
let mut outdent_capture_ix = None;
if populate_capture_indices(
&query,
language_name,
"indents",
&["start."],
&mut [
Capture::Required("indent", &mut indent_capture_ix),
Capture::Optional("start", &mut start_capture_ix),
Capture::Optional("end", &mut end_capture_ix),
Capture::Optional("outdent", &mut outdent_capture_ix),
],
) {
let mut suffixed_start_captures = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() {
if let Some(suffix) = name.strip_prefix("start.") {
suffixed_start_captures.insert(ix as u32, suffix.to_owned().into());
}
}
self.indents_config = Some(IndentConfig {
query,
indent_capture_ix,
start_capture_ix,
end_capture_ix,
outdent_capture_ix,
suffixed_start_captures,
});
}
Ok(self)
}
pub fn with_injection_query(
mut self,
source: &str,
language_name: &LanguageName,
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut language_capture_ix = None;
let mut injection_language_capture_ix = None;
let mut content_capture_ix = None;
let mut injection_content_capture_ix = None;
if populate_capture_indices(
&query,
language_name,
"injections",
&[],
&mut [
Capture::Optional("language", &mut language_capture_ix),
Capture::Optional("injection.language", &mut injection_language_capture_ix),
Capture::Optional("content", &mut content_capture_ix),
Capture::Optional("injection.content", &mut injection_content_capture_ix),
],
) {
language_capture_ix = match (language_capture_ix, injection_language_capture_ix) {
(None, Some(ix)) => Some(ix),
(Some(_), Some(_)) => {
anyhow::bail!("both language and injection.language captures are present");
}
_ => language_capture_ix,
};
content_capture_ix = match (content_capture_ix, injection_content_capture_ix) {
(None, Some(ix)) => Some(ix),
(Some(_), Some(_)) => {
anyhow::bail!("both content and injection.content captures are present")
}
_ => content_capture_ix,
};
let patterns = (0..query.pattern_count())
.map(|ix| {
let mut config = InjectionPatternConfig::default();
for setting in query.property_settings(ix) {
match setting.key.as_ref() {
"language" | "injection.language" => {
config.language.clone_from(&setting.value);
}
"combined" | "injection.combined" => {
config.combined = true;
}
_ => {}
}
}
config
})
.collect();
if let Some(content_capture_ix) = content_capture_ix {
self.injection_config = Some(InjectionConfig {
query,
language_capture_ix,
content_capture_ix,
patterns,
});
} else {
log::error!(
"missing required capture in injections {} TreeSitter query: \
content or injection.content",
language_name,
);
}
}
Ok(self)
}
pub fn with_override_query(
mut self,
source: &str,
language_name: &LanguageName,
overrides: &HashMap<String, LanguageConfigOverride>,
brackets: &mut BracketPairConfig,
scope_opt_in_language_servers: &[LanguageServerName],
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut override_configs_by_id = HashMap::default();
for (ix, mut name) in query.capture_names().iter().copied().enumerate() {
let mut range_is_inclusive = false;
if name.starts_with('_') {
continue;
}
if let Some(prefix) = name.strip_suffix(".inclusive") {
name = prefix;
range_is_inclusive = true;
}
let value = overrides.get(name).cloned().unwrap_or_default();
for server_name in &value.opt_into_language_servers {
if !scope_opt_in_language_servers.contains(server_name) {
util::debug_panic!(
"Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server"
);
}
}
override_configs_by_id.insert(
ix as u32,
OverrideEntry {
name: name.to_string(),
range_is_inclusive,
value,
},
);
}
let referenced_override_names = overrides
.keys()
.chain(brackets.disabled_scopes_by_bracket_ix.iter().flatten());
for referenced_name in referenced_override_names {
if !override_configs_by_id
.values()
.any(|entry| entry.name == *referenced_name)
{
anyhow::bail!(
"language {:?} has overrides in config not in query: {referenced_name:?}",
language_name
);
}
}
for entry in override_configs_by_id.values_mut() {
entry.value.disabled_bracket_ixs = brackets
.disabled_scopes_by_bracket_ix
.iter()
.enumerate()
.filter_map(|(ix, disabled_scope_names)| {
if disabled_scope_names.contains(&entry.name) {
Some(ix as u16)
} else {
None
}
})
.collect();
}
brackets.disabled_scopes_by_bracket_ix.clear();
self.override_config = Some(OverrideConfig {
query,
values: override_configs_by_id,
});
Ok(self)
}
pub fn with_redaction_query(
mut self,
source: &str,
language_name: &LanguageName,
) -> Result<Self> {
let query = Query::new(&self.ts_language, source)?;
let mut redaction_capture_ix = 0;
if populate_capture_indices(
&query,
language_name,
"redactions",
&[],
&mut [Capture::Required("redact", &mut redaction_capture_ix)],
) {
self.redactions_config = Some(RedactionConfig {
query,
redaction_capture_ix,
});
}
Ok(self)
}
}

View File

@@ -0,0 +1,40 @@
use std::{num::NonZeroU32, sync::Arc};
#[derive(Clone, Debug)]
pub struct HighlightMap(Arc<[Option<HighlightId>]>);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HighlightId(NonZeroU32);
impl HighlightId {
pub const TABSTOP_INSERT_ID: HighlightId = HighlightId(NonZeroU32::new(u32::MAX - 1).unwrap());
pub const TABSTOP_REPLACE_ID: HighlightId = HighlightId(NonZeroU32::new(u32::MAX - 2).unwrap());
pub fn new(capture_id: u32) -> Self {
Self(NonZeroU32::new(capture_id + 1).unwrap_or(NonZeroU32::MAX))
}
}
impl From<HighlightId> for usize {
fn from(value: HighlightId) -> Self {
value.0.get() as usize - 1
}
}
impl HighlightMap {
#[inline]
pub fn from_ids(highlight_ids: impl IntoIterator<Item = Option<HighlightId>>) -> Self {
Self(highlight_ids.into_iter().collect())
}
#[inline]
pub fn get(&self, capture_id: u32) -> Option<HighlightId> {
self.0.get(capture_id as usize).copied().flatten()
}
}
impl Default for HighlightMap {
fn default() -> Self {
Self(Arc::new([]))
}
}

View File

@@ -0,0 +1,527 @@
use crate::LanguageName;
use collections::{HashMap, HashSet, IndexSet};
use gpui_shared_string::SharedString;
use lsp::LanguageServerName;
use regex::Regex;
use schemars::{JsonSchema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::{num::NonZeroU32, path::Path, sync::Arc};
use util::serde::default_true;
/// Controls the soft-wrapping behavior in the editor.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SoftWrap {
/// Prefer a single line generally, unless an overly long line is encountered.
None,
/// Deprecated: use None instead. Left to avoid breaking existing users' configs.
/// Prefer a single line generally, unless an overly long line is encountered.
PreferLine,
/// Soft wrap lines that exceed the editor width.
EditorWidth,
/// Soft wrap line at the preferred line length or the editor width (whichever is smaller).
#[serde(alias = "preferred_line_length")]
Bounded,
}
/// Top-level configuration for a language, typically loaded from a `config.toml`
/// shipped alongside the grammar.
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct LanguageConfig {
/// Human-readable name of the language.
pub name: LanguageName,
/// The name of this language for a Markdown code fence block
pub code_fence_block_name: Option<Arc<str>>,
/// Alternative language names that Jupyter kernels may report for this language.
/// Used when a kernel's `language` field differs from Zed's language name.
/// For example, the Nu extension would set this to `["nushell"]`.
#[serde(default)]
pub kernel_language_names: Vec<Arc<str>>,
// The name of the grammar in a WASM bundle (experimental).
pub grammar: Option<Arc<str>>,
/// The criteria for matching this language to a given file.
#[serde(flatten)]
pub matcher: LanguageMatcher,
/// List of bracket types in a language.
#[serde(default)]
pub brackets: BracketPairConfig,
/// If set to true, auto indentation uses last non empty line to determine
/// the indentation level for a new line.
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
pub auto_indent_using_last_non_empty_line: bool,
// Whether indentation of pasted content should be adjusted based on the context.
#[serde(default)]
pub auto_indent_on_paste: Option<bool>,
/// A regex that is used to determine whether the indentation level should be
/// increased in the following line.
#[serde(default, deserialize_with = "deserialize_regex")]
#[schemars(schema_with = "regex_json_schema")]
pub increase_indent_pattern: Option<Regex>,
/// A regex that is used to determine whether the indentation level should be
/// decreased in the following line.
#[serde(default, deserialize_with = "deserialize_regex")]
#[schemars(schema_with = "regex_json_schema")]
pub decrease_indent_pattern: Option<Regex>,
/// A list of rules for decreasing indentation. Each rule pairs a regex with a set of valid
/// "block-starting" tokens. When a line matches a pattern, its indentation is aligned with
/// the most recent line that began with a corresponding token. This enables context-aware
/// outdenting, like aligning an `else` with its `if`.
#[serde(default)]
pub decrease_indent_patterns: Vec<DecreaseIndentConfig>,
/// A list of characters that trigger the automatic insertion of a closing
/// bracket when they immediately precede the point where an opening
/// bracket is inserted.
#[serde(default)]
pub autoclose_before: String,
/// A placeholder used internally by Semantic Index.
#[serde(default)]
pub collapsed_placeholder: String,
/// A line comment string that is inserted in e.g. `toggle comments` action.
/// A language can have multiple flavours of line comments. All of the provided line comments are
/// used for comment continuations on the next line, but only the first one is used for Editor::ToggleComments.
#[serde(default)]
pub line_comments: Vec<Arc<str>>,
/// Delimiters and configuration for recognizing and formatting block comments.
#[serde(default)]
pub block_comment: Option<BlockCommentConfig>,
/// Delimiters and configuration for recognizing and formatting documentation comments.
#[serde(default, alias = "documentation")]
pub documentation_comment: Option<BlockCommentConfig>,
/// List markers that are inserted unchanged on newline (e.g., `- `, `* `, `+ `).
#[serde(default)]
pub unordered_list: Vec<Arc<str>>,
/// Configuration for ordered lists with auto-incrementing numbers on newline (e.g., `1. ` becomes `2. `).
#[serde(default)]
pub ordered_list: Vec<OrderedListConfig>,
/// Configuration for task lists where multiple markers map to a single continuation prefix (e.g., `- [x] ` continues as `- [ ] `).
#[serde(default)]
pub task_list: Option<TaskListConfig>,
/// A list of additional regex patterns that should be treated as prefixes
/// for creating boundaries during rewrapping, ensuring content from one
/// prefixed section doesn't merge with another (e.g., markdown list items).
/// By default, Zed treats as paragraph and comment prefixes as boundaries.
#[serde(default, deserialize_with = "deserialize_regex_vec")]
#[schemars(schema_with = "regex_vec_json_schema")]
pub rewrap_prefixes: Vec<Regex>,
/// A list of language servers that are allowed to run on subranges of a given language.
#[serde(default)]
pub scope_opt_in_language_servers: Vec<LanguageServerName>,
#[serde(default)]
pub overrides: HashMap<String, LanguageConfigOverride>,
/// A list of characters that Zed should treat as word characters for the
/// purpose of features that operate on word boundaries, like 'move to next word end'
/// or a whole-word search in buffer search.
#[serde(default)]
pub word_characters: HashSet<char>,
/// Whether to indent lines using tab characters, as opposed to multiple
/// spaces.
#[serde(default)]
pub hard_tabs: Option<bool>,
/// How many columns a tab should occupy.
#[serde(default)]
#[schemars(range(min = 1, max = 128))]
pub tab_size: Option<NonZeroU32>,
/// How to soft-wrap long lines of text.
#[serde(default)]
pub soft_wrap: Option<SoftWrap>,
/// When set, selections can be wrapped using prefix/suffix pairs on both sides.
#[serde(default)]
pub wrap_characters: Option<WrapCharactersConfig>,
/// The name of a Prettier parser that will be used for this language when no file path is available.
/// If there's a parser name in the language settings, that will be used instead.
#[serde(default)]
pub prettier_parser_name: Option<String>,
/// If true, this language is only for syntax highlighting via an injection into other
/// languages, but should not appear to the user as a distinct language.
#[serde(default)]
pub hidden: bool,
/// If configured, this language contains JSX style tags, and should support auto-closing of those tags.
#[serde(default)]
pub jsx_tag_auto_close: Option<JsxTagAutoCloseConfig>,
/// A list of characters that Zed should treat as word characters for completion queries.
#[serde(default)]
pub completion_query_characters: HashSet<char>,
/// A list of characters that Zed should treat as word characters for linked edit operations.
#[serde(default)]
pub linked_edit_characters: HashSet<char>,
/// A list of preferred debuggers for this language.
#[serde(default)]
pub debuggers: IndexSet<SharedString>,
}
impl LanguageConfig {
pub const FILE_NAME: &str = "config.toml";
pub fn load(config_path: impl AsRef<Path>) -> anyhow::Result<Self> {
let config = std::fs::read_to_string(config_path.as_ref())?;
toml::from_str(&config).map_err(Into::into)
}
}
impl Default for LanguageConfig {
fn default() -> Self {
Self {
name: LanguageName::new_static(""),
code_fence_block_name: None,
kernel_language_names: Default::default(),
grammar: None,
matcher: LanguageMatcher::default(),
brackets: Default::default(),
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
auto_indent_on_paste: None,
increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(),
decrease_indent_patterns: Default::default(),
autoclose_before: Default::default(),
line_comments: Default::default(),
block_comment: Default::default(),
documentation_comment: Default::default(),
unordered_list: Default::default(),
ordered_list: Default::default(),
task_list: Default::default(),
rewrap_prefixes: Default::default(),
scope_opt_in_language_servers: Default::default(),
overrides: Default::default(),
word_characters: Default::default(),
collapsed_placeholder: Default::default(),
hard_tabs: None,
tab_size: None,
soft_wrap: None,
wrap_characters: None,
prettier_parser_name: None,
hidden: false,
jsx_tag_auto_close: None,
completion_query_characters: Default::default(),
linked_edit_characters: Default::default(),
debuggers: Default::default(),
}
}
}
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct DecreaseIndentConfig {
#[serde(default, deserialize_with = "deserialize_regex")]
#[schemars(schema_with = "regex_json_schema")]
pub pattern: Option<Regex>,
#[serde(default)]
pub valid_after: Vec<String>,
}
/// Configuration for continuing ordered lists with auto-incrementing numbers.
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct OrderedListConfig {
/// A regex pattern with a capture group for the number portion (e.g., `(\\d+)\\. `).
pub pattern: String,
/// A format string where `{1}` is replaced with the incremented number (e.g., `{1}. `).
pub format: String,
}
/// Configuration for continuing task lists on newline.
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct TaskListConfig {
/// The list markers to match (e.g., `- [ ] `, `- [x] `).
pub prefixes: Vec<Arc<str>>,
/// The marker to insert when continuing the list on a new line (e.g., `- [ ] `).
pub continuation: Arc<str>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
pub struct LanguageMatcher {
/// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`.
#[serde(default)]
pub path_suffixes: Vec<String>,
/// A regex pattern that determines whether the language should be assigned to a file or not.
#[serde(
default,
serialize_with = "serialize_regex",
deserialize_with = "deserialize_regex"
)]
#[schemars(schema_with = "regex_json_schema")]
pub first_line_pattern: Option<Regex>,
/// Alternative names for this language used in vim/emacs modelines.
/// These are matched case-insensitively against the `mode` (emacs) or
/// `filetype`/`ft` (vim) specified in the modeline.
#[serde(default)]
pub modeline_aliases: Vec<String>,
}
impl Ord for LanguageMatcher {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.path_suffixes
.cmp(&other.path_suffixes)
.then_with(|| {
self.first_line_pattern
.as_ref()
.map(Regex::as_str)
.cmp(&other.first_line_pattern.as_ref().map(Regex::as_str))
})
.then_with(|| self.modeline_aliases.cmp(&other.modeline_aliases))
}
}
impl PartialOrd for LanguageMatcher {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for LanguageMatcher {}
impl PartialEq for LanguageMatcher {
fn eq(&self, other: &Self) -> bool {
self.path_suffixes == other.path_suffixes
&& self.first_line_pattern.as_ref().map(Regex::as_str)
== other.first_line_pattern.as_ref().map(Regex::as_str)
&& self.modeline_aliases == other.modeline_aliases
}
}
/// The configuration for JSX tag auto-closing.
#[derive(Clone, Deserialize, JsonSchema, Debug)]
pub struct JsxTagAutoCloseConfig {
/// The name of the node for a opening tag
pub open_tag_node_name: String,
/// The name of the node for an closing tag
pub close_tag_node_name: String,
/// The name of the node for a complete element with children for open and close tags
pub jsx_element_node_name: String,
/// The name of the node found within both opening and closing
/// tags that describes the tag name
pub tag_name_node_name: String,
/// Alternate Node names for tag names.
/// Specifically needed as TSX represents the name in `<Foo.Bar>`
/// as `member_expression` rather than `identifier` as usual
#[serde(default)]
pub tag_name_node_name_alternates: Vec<String>,
/// Some grammars are smart enough to detect a closing tag
/// that is not valid i.e. doesn't match it's corresponding
/// opening tag or does not have a corresponding opening tag
/// This should be set to the name of the node for invalid
/// closing tags if the grammar contains such a node, otherwise
/// detecting already closed tags will not work properly
#[serde(default)]
pub erroneous_close_tag_node_name: Option<String>,
/// See above for erroneous_close_tag_node_name for details
/// This should be set if the node used for the tag name
/// within erroneous closing tags is different from the
/// normal tag name node name
#[serde(default)]
pub erroneous_close_tag_name_node_name: Option<String>,
}
/// The configuration for block comments for this language.
#[derive(Clone, Debug, JsonSchema, PartialEq)]
pub struct BlockCommentConfig {
/// A start tag of block comment.
pub start: Arc<str>,
/// A end tag of block comment.
pub end: Arc<str>,
/// A character to add as a prefix when a new line is added to a block comment.
pub prefix: Arc<str>,
/// A indent to add for prefix and end line upon new line.
#[schemars(range(min = 1, max = 128))]
pub tab_size: u32,
}
impl<'de> Deserialize<'de> for BlockCommentConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum BlockCommentConfigHelper {
New {
start: Arc<str>,
end: Arc<str>,
prefix: Arc<str>,
tab_size: u32,
},
Old([Arc<str>; 2]),
}
match BlockCommentConfigHelper::deserialize(deserializer)? {
BlockCommentConfigHelper::New {
start,
end,
prefix,
tab_size,
} => Ok(BlockCommentConfig {
start,
end,
prefix,
tab_size,
}),
BlockCommentConfigHelper::Old([start, end]) => Ok(BlockCommentConfig {
start,
end,
prefix: "".into(),
tab_size: 0,
}),
}
}
}
#[derive(Clone, Deserialize, Default, Debug, JsonSchema)]
pub struct LanguageConfigOverride {
#[serde(default)]
pub line_comments: Override<Vec<Arc<str>>>,
#[serde(default)]
pub block_comment: Override<BlockCommentConfig>,
#[serde(skip)]
pub disabled_bracket_ixs: Vec<u16>,
#[serde(default)]
pub word_characters: Override<HashSet<char>>,
#[serde(default)]
pub completion_query_characters: Override<HashSet<char>>,
#[serde(default)]
pub linked_edit_characters: Override<HashSet<char>>,
#[serde(default)]
pub opt_into_language_servers: Vec<LanguageServerName>,
#[serde(default)]
pub prefer_label_for_snippet: Option<bool>,
}
#[derive(Clone, Deserialize, Debug, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum Override<T> {
Remove { remove: bool },
Set(T),
}
impl<T> Default for Override<T> {
fn default() -> Self {
Override::Remove { remove: false }
}
}
impl<T> Override<T> {
pub fn as_option<'a>(this: Option<&'a Self>, original: Option<&'a T>) -> Option<&'a T> {
match this {
Some(Self::Set(value)) => Some(value),
Some(Self::Remove { remove: true }) => None,
Some(Self::Remove { remove: false }) | None => original,
}
}
}
/// Configuration of handling bracket pairs for a given language.
///
/// This struct includes settings for defining which pairs of characters are considered brackets and
/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
#[derive(Clone, Debug, Default, JsonSchema)]
#[schemars(with = "Vec::<BracketPairContent>")]
pub struct BracketPairConfig {
/// A list of character pairs that should be treated as brackets in the context of a given language.
pub pairs: Vec<BracketPair>,
/// A list of tree-sitter scopes for which a given bracket should not be active.
/// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
}
impl BracketPairConfig {
pub fn is_closing_brace(&self, c: char) -> bool {
self.pairs.iter().any(|pair| pair.end.starts_with(c))
}
}
#[derive(Deserialize, JsonSchema)]
pub struct BracketPairContent {
#[serde(flatten)]
pub bracket_pair: BracketPair,
#[serde(default)]
pub not_in: Vec<String>,
}
impl<'de> Deserialize<'de> for BracketPairConfig {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let result = Vec::<BracketPairContent>::deserialize(deserializer)?;
let (brackets, disabled_scopes_by_bracket_ix) = result
.into_iter()
.map(|entry| (entry.bracket_pair, entry.not_in))
.unzip();
Ok(BracketPairConfig {
pairs: brackets,
disabled_scopes_by_bracket_ix,
})
}
}
/// Describes a single bracket pair and how an editor should react to e.g. inserting
/// an opening bracket or to a newline character insertion in between `start` and `end` characters.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, JsonSchema)]
pub struct BracketPair {
/// Starting substring for a bracket.
pub start: String,
/// Ending substring for a bracket.
pub end: String,
/// True if `end` should be automatically inserted right after `start` characters.
pub close: bool,
/// True if selected text should be surrounded by `start` and `end` characters.
#[serde(default = "default_true")]
pub surround: bool,
/// True if an extra newline should be inserted while the cursor is in the middle
/// of that bracket pair.
pub newline: bool,
}
#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct WrapCharactersConfig {
/// Opening token split into a prefix and suffix. The first caret goes
/// after the prefix (i.e., between prefix and suffix).
pub start_prefix: String,
pub start_suffix: String,
/// Closing token split into a prefix and suffix. The second caret goes
/// after the prefix (i.e., between prefix and suffix).
pub end_prefix: String,
pub end_suffix: String,
}
pub fn auto_indent_using_last_non_empty_line_default() -> bool {
true
}
pub fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D::Error> {
let source = Option::<String>::deserialize(d)?;
if let Some(source) = source {
Ok(Some(regex::Regex::new(&source).map_err(de::Error::custom)?))
} else {
Ok(None)
}
}
pub fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "string"
})
}
pub fn serialize_regex<S>(regex: &Option<Regex>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match regex {
Some(regex) => serializer.serialize_str(regex.as_str()),
None => serializer.serialize_none(),
}
}
pub fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
let sources = Vec::<String>::deserialize(d)?;
sources
.into_iter()
.map(|source| regex::Regex::new(&source))
.collect::<Result<_, _>>()
.map_err(de::Error::custom)
}
pub fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
json_schema!({
"type": "array",
"items": { "type": "string" }
})
}

View File

@@ -0,0 +1,39 @@
// language_core: tree-sitter grammar infrastructure, LSP adapter traits,
// language configuration, and highlight mapping.
pub mod diagnostic;
pub mod grammar;
pub mod highlight_map;
pub mod language_config;
pub use diagnostic::{Diagnostic, DiagnosticSourceKind};
pub use grammar::{
BracketsConfig, BracketsPatternConfig, DebugVariablesConfig, DebuggerTextObject, Grammar,
GrammarId, HighlightsConfig, IndentConfig, InjectionConfig, InjectionPatternConfig,
NEXT_GRAMMAR_ID, OutlineConfig, OverrideConfig, OverrideEntry, RedactionConfig,
RunnableCapture, RunnableConfig, TextObject, TextObjectConfig,
};
pub use highlight_map::{HighlightId, HighlightMap};
pub use language_config::{
BlockCommentConfig, BracketPair, BracketPairConfig, BracketPairContent, DecreaseIndentConfig,
JsxTagAutoCloseConfig, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
OrderedListConfig, Override, SoftWrap, TaskListConfig, WrapCharactersConfig,
auto_indent_using_last_non_empty_line_default, deserialize_regex, deserialize_regex_vec,
regex_json_schema, regex_vec_json_schema, serialize_regex,
};
pub mod code_label;
pub mod language_name;
pub mod lsp_adapter;
pub mod manifest;
pub mod queries;
pub mod toolchain;
pub use code_label::{CodeLabel, CodeLabelBuilder, Symbol};
pub use language_name::{LanguageId, LanguageName};
pub use lsp_adapter::{
BinaryStatus, LanguageServerStatusUpdate, PromptResponseContext, ServerHealth, ToLspPosition,
};
pub use manifest::ManifestName;
pub use queries::{LanguageQueries, QUERY_FILENAME_PREFIXES};
pub use toolchain::{Toolchain, ToolchainList, ToolchainMetadata, ToolchainScope};

View File

@@ -0,0 +1,109 @@
use gpui_shared_string::SharedString;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
borrow::Borrow,
sync::atomic::{AtomicUsize, Ordering::SeqCst},
};
static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct LanguageId(usize);
impl LanguageId {
pub fn new() -> Self {
Self(NEXT_LANGUAGE_ID.fetch_add(1, SeqCst))
}
}
impl Default for LanguageId {
fn default() -> Self {
Self::new()
}
}
#[derive(
Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
pub struct LanguageName(pub SharedString);
impl LanguageName {
pub fn new(s: &str) -> Self {
Self(SharedString::new(s))
}
pub fn new_static(s: &'static str) -> Self {
Self(SharedString::new_static(s))
}
pub fn from_proto(s: String) -> Self {
Self(SharedString::from(s))
}
pub fn to_proto(&self) -> String {
self.0.to_string()
}
pub fn lsp_id(&self) -> String {
match self.0.as_ref() {
"Plain Text" => "plaintext".to_string(),
language_name => language_name.to_lowercase(),
}
}
}
impl From<LanguageName> for SharedString {
fn from(value: LanguageName) -> Self {
value.0
}
}
impl From<SharedString> for LanguageName {
fn from(value: SharedString) -> Self {
LanguageName(value)
}
}
impl AsRef<str> for LanguageName {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl Borrow<str> for LanguageName {
fn borrow(&self) -> &str {
self.0.as_ref()
}
}
impl PartialEq<str> for LanguageName {
fn eq(&self, other: &str) -> bool {
self.0.as_ref() == other
}
}
impl PartialEq<&str> for LanguageName {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref() == *other
}
}
impl std::fmt::Display for LanguageName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&'static str> for LanguageName {
fn from(str: &'static str) -> Self {
Self(SharedString::new_static(str))
}
}
impl From<LanguageName> for String {
fn from(value: LanguageName) -> Self {
let value: &str = &value.0;
Self::from(value)
}
}

View File

@@ -0,0 +1,44 @@
use gpui_shared_string::SharedString;
use serde::{Deserialize, Serialize};
/// Converts a value into an LSP position.
pub trait ToLspPosition {
/// Converts the value into an LSP position.
fn to_lsp_position(self) -> lsp::Position;
}
/// Context provided to LSP adapters when a user responds to a ShowMessageRequest prompt.
/// This allows adapters to intercept preference selections (like "Always" or "Never")
/// and potentially persist them to Zed's settings.
#[derive(Debug, Clone)]
pub struct PromptResponseContext {
/// The original message shown to the user
pub message: String,
/// The action (button) the user selected
pub selected_action: lsp::MessageActionItem,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LanguageServerStatusUpdate {
Binary(BinaryStatus),
Health(ServerHealth, Option<SharedString>),
}
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone, Copy)]
#[serde(rename_all = "camelCase")]
pub enum ServerHealth {
Ok,
Warning,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BinaryStatus {
None,
CheckingForUpdate,
Downloading,
Starting,
Stopping,
Stopped,
Failed { error: String },
}

View File

@@ -0,0 +1,36 @@
use std::borrow::Borrow;
use gpui_shared_string::SharedString;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ManifestName(SharedString);
impl Borrow<SharedString> for ManifestName {
fn borrow(&self) -> &SharedString {
&self.0
}
}
impl Borrow<str> for ManifestName {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<SharedString> for ManifestName {
fn from(value: SharedString) -> Self {
Self(value)
}
}
impl From<ManifestName> for SharedString {
fn from(value: ManifestName) -> Self {
value.0
}
}
impl AsRef<SharedString> for ManifestName {
fn as_ref(&self) -> &SharedString {
&self.0
}
}

View File

@@ -0,0 +1,31 @@
use std::borrow::Cow;
pub type QueryFieldAccessor = fn(&mut LanguageQueries) -> &mut Option<Cow<'static, str>>;
pub const QUERY_FILENAME_PREFIXES: &[(&str, QueryFieldAccessor)] = &[
("highlights", |q| &mut q.highlights),
("brackets", |q| &mut q.brackets),
("outline", |q| &mut q.outline),
("indents", |q| &mut q.indents),
("injections", |q| &mut q.injections),
("overrides", |q| &mut q.overrides),
("redactions", |q| &mut q.redactions),
("runnables", |q| &mut q.runnables),
("debugger", |q| &mut q.debugger),
("textobjects", |q| &mut q.text_objects),
];
/// Tree-sitter language queries for a given language.
#[derive(Debug, Default)]
pub struct LanguageQueries {
pub highlights: Option<Cow<'static, str>>,
pub brackets: Option<Cow<'static, str>>,
pub indents: Option<Cow<'static, str>>,
pub outline: Option<Cow<'static, str>>,
pub injections: Option<Cow<'static, str>>,
pub overrides: Option<Cow<'static, str>>,
pub redactions: Option<Cow<'static, str>>,
pub runnables: Option<Cow<'static, str>>,
pub text_objects: Option<Cow<'static, str>>,
pub debugger: Option<Cow<'static, str>>,
}

View File

@@ -0,0 +1,124 @@
//! Provides core data types for language toolchains.
//!
//! A language can have associated toolchains,
//! which is a set of tools used to interact with the projects written in said language.
//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
use std::{path::Path, sync::Arc};
use gpui_shared_string::SharedString;
use util::rel_path::RelPath;
use crate::{LanguageName, ManifestName};
/// Represents a single toolchain.
#[derive(Clone, Eq, Debug)]
pub struct Toolchain {
/// User-facing label
pub name: SharedString,
/// Absolute path
pub path: SharedString,
pub language_name: LanguageName,
/// Full toolchain data (including language-specific details)
pub as_json: serde_json::Value,
}
impl std::hash::Hash for Toolchain {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let Self {
name,
path,
language_name,
as_json: _,
} = self;
name.hash(state);
path.hash(state);
language_name.hash(state);
}
}
impl PartialEq for Toolchain {
fn eq(&self, other: &Self) -> bool {
let Self {
name,
path,
language_name,
as_json: _,
} = self;
// Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
// Thus, there could be multiple entries that look the same in the UI.
(name, path, language_name).eq(&(&other.name, &other.path, &other.language_name))
}
}
/// Declares a scope of a toolchain added by user.
///
/// When the user adds a toolchain, we give them an option to see that toolchain in:
/// - All of their projects
/// - A project they're currently in.
/// - Only in the subproject they're currently in.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ToolchainScope {
Subproject(Arc<Path>, Arc<RelPath>),
Project,
/// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
Global,
}
impl ToolchainScope {
pub fn label(&self) -> &'static str {
match self {
ToolchainScope::Subproject(_, _) => "Subproject",
ToolchainScope::Project => "Project",
ToolchainScope::Global => "Global",
}
}
pub fn description(&self) -> &'static str {
match self {
ToolchainScope::Subproject(_, _) => {
"Available only in the subproject you're currently in."
}
ToolchainScope::Project => "Available in all locations in your current project.",
ToolchainScope::Global => "Available in all of your projects on this machine.",
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ToolchainMetadata {
/// Returns a term which we should use in UI to refer to toolchains produced by a given `ToolchainLister`.
pub term: SharedString,
/// A user-facing placeholder describing the semantic meaning of a path to a new toolchain.
pub new_toolchain_placeholder: SharedString,
/// The name of the manifest file for this toolchain.
pub manifest_name: ManifestName,
}
type DefaultIndex = usize;
#[derive(Default, Clone, Debug)]
pub struct ToolchainList {
pub toolchains: Vec<Toolchain>,
pub default: Option<DefaultIndex>,
pub groups: Box<[(usize, SharedString)]>,
}
impl ToolchainList {
pub fn toolchains(&self) -> &[Toolchain] {
&self.toolchains
}
pub fn default_toolchain(&self) -> Option<Toolchain> {
self.default.and_then(|ix| self.toolchains.get(ix)).cloned()
}
pub fn group_for_index(&self, index: usize) -> Option<(usize, SharedString)> {
if index >= self.toolchains.len() {
return None;
}
let first_equal_or_greater = self
.groups
.partition_point(|(group_lower_bound, _)| group_lower_bound <= &index);
self.groups
.get(first_equal_or_greater.checked_sub(1)?)
.cloned()
}
}