Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
* Add `thing_layout_edges` to affect node ranks without rendering any visible `<path>`s. ([#61][#61])
* Reverse path calculation computation when `to` node has a smaller node rank than the `from` node, to reduce unnecessary spacer routing. ([#62][#62])
* Update `EdgeCurvature::Curved` and `EdgeCurvature::Orthogonal` edge path builders to skip thing description spacers when rank dir spacers are at a larger cross-axis coordinate. ([#62][#62])
* Suggest custom entity types as a completion option when editing `theme_types_styles`. ([#63][#63])
* Suggest custom entity types as a completion option when editing `entity_types.<entity_name>.<entity_type_list>`. ([#63][#63])

[#42]: https://github.com/azriel91/disposition/pull/42
[#43]: https://github.com/azriel91/disposition/pull/43
Expand All @@ -89,6 +91,7 @@
[#60]: https://github.com/azriel91/disposition/pull/60
[#61]: https://github.com/azriel91/disposition/pull/61
[#62]: https://github.com/azriel91/disposition/pull/62
[#63]: https://github.com/azriel91/disposition/pull/63


## 0.3.0 (2026-06-07)
Expand Down
14 changes: 3 additions & 11 deletions crate/lsp/src/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

use async_lsp::lsp_types::{Position, Range, TextEdit};

use crate::completion::yaml_lines::{indent, is_blank_or_comment, is_list_item, line_map_key};
use crate::completion::yaml_lines::{
indent, is_blank_or_comment, is_list_item, line_map_key, split_flow_items,
};

pub use self::list_conversion::ListConversion;

Expand Down Expand Up @@ -162,16 +164,6 @@ impl CodeActionEngine {
}
}

/// Splits the inside of a flow sequence (`a, b, c`) into trimmed, non-empty
/// items.
fn split_flow_items(inner: &str) -> Vec<&str> {
inner
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
.collect()
}

/// The range covering the whole of `line` at `line_idx` (column 0 to its end).
fn whole_line_range(line_idx: usize, line: &str) -> Range {
Range {
Expand Down
13 changes: 13 additions & 0 deletions crate/lsp/src/completion/completion_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl CompletionEngine {
);
}

completion_items_dedup(&mut items);
items
}

Expand Down Expand Up @@ -227,6 +228,7 @@ impl CompletionEngine {
}));
}

completion_items_dedup(&mut items);
items
}

Expand Down Expand Up @@ -259,6 +261,17 @@ impl CompletionEngine {
}
}

/// Removes items whose `label` already appeared earlier in `items`.
///
/// Document-derived and schema-derived suggestions are merged independently
/// and can coincide -- e.g. a document declares a custom entity type whose
/// name happens to match a built-in `EntityType` const -- so this dedups the
/// combined list by label, keeping the first occurrence.
fn completion_items_dedup(items: &mut Vec<CompletionItem>) {
let mut seen = BTreeSet::new();
items.retain(|item| seen.insert(item.label.clone()));
}

/// Builds the `insert_text` for a value completion `label`.
///
/// Returns `None` when the bare `label` can be inserted as-is. Otherwise the
Expand Down
60 changes: 56 additions & 4 deletions crate/lsp/src/completion/dynamic_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::collections::BTreeSet;
use crate::completion::{
id_category::IdCategory,
key_category::KeyCategory,
yaml_lines::{indent, is_blank_or_comment, line_map_key},
yaml_lines::{indent, is_blank_or_comment, is_list_item, line_map_key, split_flow_items},
};

/// IDs defined in the document, grouped by category.
Expand All @@ -27,6 +27,10 @@ pub struct DynamicCompletions {
/// `EdgeGroupId`s declared under `thing_dependencies` /
/// `thing_interactions`.
edge_group_ids: BTreeSet<String>,
/// Custom `type_*` ids declared as list items under any `entity_types`
/// entry (e.g. `type_organisation` from `entity_types.t_aws:
/// [type_organisation]`).
entity_type_ids: BTreeSet<String>,
}

impl DynamicCompletions {
Expand All @@ -49,12 +53,15 @@ impl DynamicCompletions {
let mut edge_group_ids = collect_block_keys(&lines, "thing_dependencies", true, true);
edge_group_ids.extend(collect_block_keys(&lines, "thing_interactions", true, true));

let entity_type_ids = collect_entity_types_values(&lines);

DynamicCompletions {
thing_ids,
tag_ids,
process_ids,
step_ids,
edge_group_ids,
entity_type_ids,
}
}

Expand All @@ -65,6 +72,7 @@ impl DynamicCompletions {
IdCategory::Tag => self.tag_ids.iter().map(String::as_str).collect(),
IdCategory::ProcessStep => self.step_ids.iter().map(String::as_str).collect(),
IdCategory::EdgeGroup => self.edge_group_ids.iter().map(String::as_str).collect(),
IdCategory::EntityType => self.entity_type_ids.iter().map(String::as_str).collect(),
IdCategory::Any => self
.thing_ids
.iter()
Expand All @@ -81,7 +89,9 @@ impl DynamicCompletions {
/// These are the document-derived IDs, templated IDs, and known literal
/// keys. Schema-derived suggestions (the built-in `StyleAlias` /
/// `EntityType` keys) are *not* included here -- the completion engine adds
/// those from the schema, which this type does not have access to.
/// those from the schema, which this type does not have access to. Custom
/// `EntityType`s declared in `entity_types`, however, *are*
/// document-derived and so are included here.
pub fn key_suggestions(&self, key_category: KeyCategory) -> Vec<String> {
let owned = |ids: Vec<&str>| ids.into_iter().map(str::to_string).collect::<Vec<String>>();

Expand Down Expand Up @@ -113,8 +123,9 @@ impl DynamicCompletions {
suggestions.extend(owned(self.ids_for(IdCategory::Tag)));
suggestions
}
// Built-in entity types come from the schema, added by the engine.
KeyCategory::EntityType => Vec::new(),
// Custom entity types declared in `entity_types`; built-in
// entity types come from the schema, added by the engine.
KeyCategory::EntityType => owned(self.ids_for(IdCategory::EntityType)),
}
}

Expand Down Expand Up @@ -207,3 +218,44 @@ fn collect_block_keys(

keys
}

/// Collects the custom entity type ids (`type_*` list items) declared under
/// each entry of the top-level `entity_types` block, e.g. `type_organisation`
/// from `entity_types.t_aws: [type_organisation]` or a block-sequence
/// `- type_organisation` item.
fn collect_entity_types_values(lines: &[&str]) -> BTreeSet<String> {
let mut values = BTreeSet::new();

let Some(block_start) = lines.iter().position(|line| {
!is_blank_or_comment(line)
&& indent(line) == 0
&& line_map_key(line).as_deref() == Some("entity_types")
}) else {
return values;
};

for line in &lines[block_start + 1..] {
if is_blank_or_comment(line) {
continue;
}
if indent(line) == 0 {
break;
}

if is_list_item(line) {
let value = line.trim_start().trim_start_matches('-').trim();
if !value.is_empty() {
values.insert(value.to_string());
}
} else if line_map_key(line).is_some()
&& let Some(colon_idx) = line.find(':')
{
let value = line[colon_idx + 1..].trim();
if let Some(inner) = value.strip_prefix('[').and_then(|v| v.strip_suffix(']')) {
values.extend(split_flow_items(inner).into_iter().map(str::to_string));
}
}
}

values
}
4 changes: 4 additions & 0 deletions crate/lsp/src/completion/id_category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ pub enum IdCategory {
EdgeGroup,
/// Any ID -- the generic `Id` type (e.g. `thing_layouts` keys).
Any,
/// An `EntityType` custom `type_*` id -- declared as a list item under
/// any `entity_types` entry.
EntityType,
}

impl IdCategory {
Expand All @@ -30,6 +33,7 @@ impl IdCategory {
"ProcessStepId" => Some(IdCategory::ProcessStep),
"EdgeGroupId" => Some(IdCategory::EdgeGroup),
"Id" => Some(IdCategory::Any),
"EntityType" => Some(IdCategory::EntityType),
_ => None,
}
}
Expand Down
3 changes: 2 additions & 1 deletion crate/lsp/src/completion/key_category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ pub enum KeyCategory {
/// `tag_defaults` plus the tags defined in the document.
TagFocus,
/// Keys are `EntityType`s -- `theme_types_styles`. Offers the built-in
/// entity types.
/// entity types plus any custom `type_*` ids declared under
/// `entity_types` in the document.
EntityType,
}

Expand Down
10 changes: 10 additions & 0 deletions crate/lsp/src/completion/yaml_lines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ pub fn is_list_item(line: &str) -> bool {
trimmed == "-" || trimmed.starts_with("- ")
}

/// Splits the inside of a flow sequence (`a, b, c`) into trimmed, non-empty
/// items.
pub fn split_flow_items(inner: &str) -> Vec<&str> {
inner
.split(',')
.map(str::trim)
.filter(|item| !item.is_empty())
.collect()
}

/// Returns the map key declared on `line` (`key:` or `key: value`), or `None`
/// for blank / comment / list-item / non-key lines.
pub fn line_map_key(line: &str) -> Option<String> {
Expand Down
25 changes: 25 additions & 0 deletions workspace_tests/src/lsp/completion_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,31 @@ fn builtin_entity_types_offered_in_theme_types_styles() {
);
}

#[test]
fn custom_entity_types_offered_in_theme_types_styles() {
let text = "entity_types:\n t_aws: [type_organisation]\n\
theme_types_styles:\n ";
let labels = labels(text, 3, 2);

for expected in ["type_organisation", "type_thing_default"] {
assert!(
labels.iter().any(|label| label == expected),
"expected entity type `{expected}` in {labels:?}"
);
}
}

#[test]
fn custom_entity_types_offered_in_entity_types_values() {
let text = "entity_types:\n t_a: [type_organisation]\n t_b:\n - ";
let labels = labels(text, 3, 6);

assert!(
labels.iter().any(|label| label == "type_organisation"),
"expected custom entity type `type_organisation` in {labels:?}"
);
}

#[test]
fn already_declared_keys_filtered_from_suggestions() {
// `t_a` is already a `thing_names` key, so only `t_b` should remain.
Expand Down
23 changes: 23 additions & 0 deletions workspace_tests/src/lsp/dynamic_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,26 @@ fn key_suggestions_style_alias_offers_custom_placeholder() {
dynamic_completions.key_suggestions(KeyCategory::StyleAlias)
);
}

#[test]
fn collects_entity_type_ids_from_flow_and_block_entity_types_lists() {
let text = "entity_types:\n t_aws: [type_organisation]\n t_github:\n \
- type_organisation\n - type_open_source\n";
let dynamic_completions = DynamicCompletions::from_text(text);

assert_eq!(
vec!["type_open_source", "type_organisation"],
dynamic_completions.ids_for(IdCategory::EntityType)
);
}

#[test]
fn key_suggestions_entity_type_includes_custom_types_from_entity_types() {
let text = "entity_types:\n t_aws: [type_organisation]\n";
let dynamic_completions = DynamicCompletions::from_text(text);

assert_eq!(
vec!["type_organisation".to_string()],
dynamic_completions.key_suggestions(KeyCategory::EntityType)
);
}
Loading