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:
27
crates/language_core/Cargo.toml
Normal file
27
crates/language_core/Cargo.toml
Normal 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 = []
|
||||
1
crates/language_core/LICENSE-GPL
Symbolic link
1
crates/language_core/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
122
crates/language_core/src/code_label.rs
Normal file
122
crates/language_core/src/code_label.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
76
crates/language_core/src/diagnostic.rs
Normal file
76
crates/language_core/src/diagnostic.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
755
crates/language_core/src/grammar.rs
Normal file
755
crates/language_core/src/grammar.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
40
crates/language_core/src/highlight_map.rs
Normal file
40
crates/language_core/src/highlight_map.rs
Normal 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([]))
|
||||
}
|
||||
}
|
||||
527
crates/language_core/src/language_config.rs
Normal file
527
crates/language_core/src/language_config.rs
Normal 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" }
|
||||
})
|
||||
}
|
||||
39
crates/language_core/src/language_core.rs
Normal file
39
crates/language_core/src/language_core.rs
Normal 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};
|
||||
109
crates/language_core/src/language_name.rs
Normal file
109
crates/language_core/src/language_name.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
44
crates/language_core/src/lsp_adapter.rs
Normal file
44
crates/language_core/src/lsp_adapter.rs
Normal 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 },
|
||||
}
|
||||
36
crates/language_core/src/manifest.rs
Normal file
36
crates/language_core/src/manifest.rs
Normal 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
|
||||
}
|
||||
}
|
||||
31
crates/language_core/src/queries.rs
Normal file
31
crates/language_core/src/queries.rs
Normal 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>>,
|
||||
}
|
||||
124
crates/language_core/src/toolchain.rs
Normal file
124
crates/language_core/src/toolchain.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user