Skip to content

Commit 08a2a3b

Browse files
authored
Merge pull request #63 from azriel91/feature/lsp-suggest-custom-entity-types
Suggest custom entity types as a completion option when editing `theme_types_styles`.
2 parents 716ea84 + 1ddb4de commit 08a2a3b

9 files changed

Lines changed: 139 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
* Add `thing_layout_edges` to affect node ranks without rendering any visible `<path>`s. ([#61][#61])
6868
* Reverse path calculation computation when `to` node has a smaller node rank than the `from` node, to reduce unnecessary spacer routing. ([#62][#62])
6969
* 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])
70+
* Suggest custom entity types as a completion option when editing `theme_types_styles`. ([#63][#63])
71+
* Suggest custom entity types as a completion option when editing `entity_types.<entity_name>.<entity_type_list>`. ([#63][#63])
7072

7173
[#42]: https://github.com/azriel91/disposition/pull/42
7274
[#43]: https://github.com/azriel91/disposition/pull/43
@@ -89,6 +91,7 @@
8991
[#60]: https://github.com/azriel91/disposition/pull/60
9092
[#61]: https://github.com/azriel91/disposition/pull/61
9193
[#62]: https://github.com/azriel91/disposition/pull/62
94+
[#63]: https://github.com/azriel91/disposition/pull/63
9295

9396

9497
## 0.3.0 (2026-06-07)

crate/lsp/src/code_action.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
1212
use async_lsp::lsp_types::{Position, Range, TextEdit};
1313

14-
use crate::completion::yaml_lines::{indent, is_blank_or_comment, is_list_item, line_map_key};
14+
use crate::completion::yaml_lines::{
15+
indent, is_blank_or_comment, is_list_item, line_map_key, split_flow_items,
16+
};
1517

1618
pub use self::list_conversion::ListConversion;
1719

@@ -162,16 +164,6 @@ impl CodeActionEngine {
162164
}
163165
}
164166

165-
/// Splits the inside of a flow sequence (`a, b, c`) into trimmed, non-empty
166-
/// items.
167-
fn split_flow_items(inner: &str) -> Vec<&str> {
168-
inner
169-
.split(',')
170-
.map(str::trim)
171-
.filter(|item| !item.is_empty())
172-
.collect()
173-
}
174-
175167
/// The range covering the whole of `line` at `line_idx` (column 0 to its end).
176168
fn whole_line_range(line_idx: usize, line: &str) -> Range {
177169
Range {

crate/lsp/src/completion/completion_engine.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ impl CompletionEngine {
150150
);
151151
}
152152

153+
completion_items_dedup(&mut items);
153154
items
154155
}
155156

@@ -227,6 +228,7 @@ impl CompletionEngine {
227228
}));
228229
}
229230

231+
completion_items_dedup(&mut items);
230232
items
231233
}
232234

@@ -259,6 +261,17 @@ impl CompletionEngine {
259261
}
260262
}
261263

264+
/// Removes items whose `label` already appeared earlier in `items`.
265+
///
266+
/// Document-derived and schema-derived suggestions are merged independently
267+
/// and can coincide -- e.g. a document declares a custom entity type whose
268+
/// name happens to match a built-in `EntityType` const -- so this dedups the
269+
/// combined list by label, keeping the first occurrence.
270+
fn completion_items_dedup(items: &mut Vec<CompletionItem>) {
271+
let mut seen = BTreeSet::new();
272+
items.retain(|item| seen.insert(item.label.clone()));
273+
}
274+
262275
/// Builds the `insert_text` for a value completion `label`.
263276
///
264277
/// Returns `None` when the bare `label` can be inserted as-is. Otherwise the

crate/lsp/src/completion/dynamic_completions.rs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::collections::BTreeSet;
1010
use crate::completion::{
1111
id_category::IdCategory,
1212
key_category::KeyCategory,
13-
yaml_lines::{indent, is_blank_or_comment, line_map_key},
13+
yaml_lines::{indent, is_blank_or_comment, is_list_item, line_map_key, split_flow_items},
1414
};
1515

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

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

56+
let entity_type_ids = collect_entity_types_values(&lines);
57+
5258
DynamicCompletions {
5359
thing_ids,
5460
tag_ids,
5561
process_ids,
5662
step_ids,
5763
edge_group_ids,
64+
entity_type_ids,
5865
}
5966
}
6067

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

@@ -113,8 +123,9 @@ impl DynamicCompletions {
113123
suggestions.extend(owned(self.ids_for(IdCategory::Tag)));
114124
suggestions
115125
}
116-
// Built-in entity types come from the schema, added by the engine.
117-
KeyCategory::EntityType => Vec::new(),
126+
// Custom entity types declared in `entity_types`; built-in
127+
// entity types come from the schema, added by the engine.
128+
KeyCategory::EntityType => owned(self.ids_for(IdCategory::EntityType)),
118129
}
119130
}
120131

@@ -207,3 +218,44 @@ fn collect_block_keys(
207218

208219
keys
209220
}
221+
222+
/// Collects the custom entity type ids (`type_*` list items) declared under
223+
/// each entry of the top-level `entity_types` block, e.g. `type_organisation`
224+
/// from `entity_types.t_aws: [type_organisation]` or a block-sequence
225+
/// `- type_organisation` item.
226+
fn collect_entity_types_values(lines: &[&str]) -> BTreeSet<String> {
227+
let mut values = BTreeSet::new();
228+
229+
let Some(block_start) = lines.iter().position(|line| {
230+
!is_blank_or_comment(line)
231+
&& indent(line) == 0
232+
&& line_map_key(line).as_deref() == Some("entity_types")
233+
}) else {
234+
return values;
235+
};
236+
237+
for line in &lines[block_start + 1..] {
238+
if is_blank_or_comment(line) {
239+
continue;
240+
}
241+
if indent(line) == 0 {
242+
break;
243+
}
244+
245+
if is_list_item(line) {
246+
let value = line.trim_start().trim_start_matches('-').trim();
247+
if !value.is_empty() {
248+
values.insert(value.to_string());
249+
}
250+
} else if line_map_key(line).is_some()
251+
&& let Some(colon_idx) = line.find(':')
252+
{
253+
let value = line[colon_idx + 1..].trim();
254+
if let Some(inner) = value.strip_prefix('[').and_then(|v| v.strip_suffix(']')) {
255+
values.extend(split_flow_items(inner).into_iter().map(str::to_string));
256+
}
257+
}
258+
}
259+
260+
values
261+
}

crate/lsp/src/completion/id_category.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ pub enum IdCategory {
1919
EdgeGroup,
2020
/// Any ID -- the generic `Id` type (e.g. `thing_layouts` keys).
2121
Any,
22+
/// An `EntityType` custom `type_*` id -- declared as a list item under
23+
/// any `entity_types` entry.
24+
EntityType,
2225
}
2326

2427
impl IdCategory {
@@ -30,6 +33,7 @@ impl IdCategory {
3033
"ProcessStepId" => Some(IdCategory::ProcessStep),
3134
"EdgeGroupId" => Some(IdCategory::EdgeGroup),
3235
"Id" => Some(IdCategory::Any),
36+
"EntityType" => Some(IdCategory::EntityType),
3337
_ => None,
3438
}
3539
}

crate/lsp/src/completion/key_category.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ pub enum KeyCategory {
4444
/// `tag_defaults` plus the tags defined in the document.
4545
TagFocus,
4646
/// Keys are `EntityType`s -- `theme_types_styles`. Offers the built-in
47-
/// entity types.
47+
/// entity types plus any custom `type_*` ids declared under
48+
/// `entity_types` in the document.
4849
EntityType,
4950
}
5051

crate/lsp/src/completion/yaml_lines.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ pub fn is_list_item(line: &str) -> bool {
2121
trimmed == "-" || trimmed.starts_with("- ")
2222
}
2323

24+
/// Splits the inside of a flow sequence (`a, b, c`) into trimmed, non-empty
25+
/// items.
26+
pub fn split_flow_items(inner: &str) -> Vec<&str> {
27+
inner
28+
.split(',')
29+
.map(str::trim)
30+
.filter(|item| !item.is_empty())
31+
.collect()
32+
}
33+
2434
/// Returns the map key declared on `line` (`key:` or `key: value`), or `None`
2535
/// for blank / comment / list-item / non-key lines.
2636
pub fn line_map_key(line: &str) -> Option<String> {

workspace_tests/src/lsp/completion_engine.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,31 @@ fn builtin_entity_types_offered_in_theme_types_styles() {
341341
);
342342
}
343343

344+
#[test]
345+
fn custom_entity_types_offered_in_theme_types_styles() {
346+
let text = "entity_types:\n t_aws: [type_organisation]\n\
347+
theme_types_styles:\n ";
348+
let labels = labels(text, 3, 2);
349+
350+
for expected in ["type_organisation", "type_thing_default"] {
351+
assert!(
352+
labels.iter().any(|label| label == expected),
353+
"expected entity type `{expected}` in {labels:?}"
354+
);
355+
}
356+
}
357+
358+
#[test]
359+
fn custom_entity_types_offered_in_entity_types_values() {
360+
let text = "entity_types:\n t_a: [type_organisation]\n t_b:\n - ";
361+
let labels = labels(text, 3, 6);
362+
363+
assert!(
364+
labels.iter().any(|label| label == "type_organisation"),
365+
"expected custom entity type `type_organisation` in {labels:?}"
366+
);
367+
}
368+
344369
#[test]
345370
fn already_declared_keys_filtered_from_suggestions() {
346371
// `t_a` is already a `thing_names` key, so only `t_b` should remain.

workspace_tests/src/lsp/dynamic_completions.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,26 @@ fn key_suggestions_style_alias_offers_custom_placeholder() {
181181
dynamic_completions.key_suggestions(KeyCategory::StyleAlias)
182182
);
183183
}
184+
185+
#[test]
186+
fn collects_entity_type_ids_from_flow_and_block_entity_types_lists() {
187+
let text = "entity_types:\n t_aws: [type_organisation]\n t_github:\n \
188+
- type_organisation\n - type_open_source\n";
189+
let dynamic_completions = DynamicCompletions::from_text(text);
190+
191+
assert_eq!(
192+
vec!["type_open_source", "type_organisation"],
193+
dynamic_completions.ids_for(IdCategory::EntityType)
194+
);
195+
}
196+
197+
#[test]
198+
fn key_suggestions_entity_type_includes_custom_types_from_entity_types() {
199+
let text = "entity_types:\n t_aws: [type_organisation]\n";
200+
let dynamic_completions = DynamicCompletions::from_text(text);
201+
202+
assert_eq!(
203+
vec!["type_organisation".to_string()],
204+
dynamic_completions.key_suggestions(KeyCategory::EntityType)
205+
);
206+
}

0 commit comments

Comments
 (0)