diff --git a/crates/workshop-rs/src/lookup.rs b/crates/workshop-rs/src/lookup.rs index e20e07e7..01972c74 100644 --- a/crates/workshop-rs/src/lookup.rs +++ b/crates/workshop-rs/src/lookup.rs @@ -11,7 +11,7 @@ //! vocabulary. Consumers compose and present the matches; this module owns //! no data of its own. -use crate::catalog::{Catalog, CatalogEntry, Kind, Locale}; +use crate::catalog::{Catalog, CatalogEntry, EnumDomain, Kind, Locale}; use crate::core::error::{Result, WorkshopError}; use crate::core::suggest; use crate::settings::{self, PathPart, SettingDefinition, SettingValueDomain, table}; @@ -178,15 +178,7 @@ impl Catalog { /// ))); /// ``` pub fn lookup(&self, locale: &Locale, query: &str) -> Result> { - if !self.supports(locale) { - return Err(WorkshopError::unsupported( - format!( - "lookup for locale '{locale}' is not supported: \ - the catalog does not declare it" - ), - None, - )); - } + self.check_locale(locale)?; let primary = self.primary_locale(); let distinct_locale = locale != primary; let prepared = PreparedQuery::new(query); @@ -204,15 +196,7 @@ impl Catalog { .filter_map(|text| prepared.name_score(text)) .min(); if let Some(score) = score { - matches.push(( - score, - LookupMatch::Builtin { - kind: entry.kind, - id: entry.id.clone(), - display_name: entry.spelling(locale).map(String::from), - signature: signature(self, entry, locale), - }, - )); + matches.push((score, self.builtin_match(entry, locale))); } } for domain in self.enum_domains() { @@ -222,14 +206,7 @@ impl Catalog { .filter_map(|text| prepared.name_score(text)) .min(); if let Some(score) = score { - matches.push(( - score, - LookupMatch::EnumDomain { - domain: domain.domain.clone(), - display_name: domain.spelling(locale).map(String::from), - members: self.enum_members(&domain.domain, locale), - }, - )); + matches.push((score, self.enum_domain_match(domain, locale))); } } for domain in self.enum_domains() { @@ -332,15 +309,7 @@ impl Catalog { within: &str, query: Option<&str>, ) -> Result> { - if !self.supports(locale) { - return Err(WorkshopError::unsupported( - format!( - "lookup for locale '{locale}' is not supported: \ - the catalog does not declare it" - ), - None, - )); - } + self.check_locale(locale)?; let prepared = query .filter(|query| !query.is_empty()) .map(PreparedQuery::new); @@ -357,6 +326,88 @@ impl Catalog { Ok(scored.into_iter().map(|(_, lookup)| lookup).collect()) } + /// Every entry of a listable `kind` under `locale` — one + /// [`LookupMatch`] per catalog entry of the kind, in catalog order, + /// with no query, matching, or ranking. + /// + /// `Kind::Action`, `Kind::Value`, and `Kind::Event` list one + /// [`LookupMatch::Builtin`] per entry — the same match + /// [`Catalog::lookup`] builds for that entry, signature included. + /// `Kind::Enum` lists one [`LookupMatch::EnumDomain`] per declared + /// enum domain, in [`Catalog::enum_domains`] order. Enum members and + /// call parameters are scoped children that already list through + /// [`Catalog::lookup_within`]; the other kinds have no listing + /// surface, and naming one is an `Err`. + /// + /// `Err` reports a locale the catalog does not declare or a `kind` + /// with no listing surface. The result is the complete listable + /// vocabulary of the kind: paging and presentation belong to the + /// consumer. + /// + /// ``` + /// use workshop_rs::catalog::{Catalog, Kind, Locale}; + /// use workshop_rs::lookup::LookupMatch; + /// + /// let catalog = Catalog::builtin().unwrap(); + /// let events = catalog + /// .lookup_entries(&Locale::new("en-US"), Kind::Event) + /// .unwrap(); + /// assert!(events.iter().all( + /// |m| matches!(m, LookupMatch::Builtin { kind: Kind::Event, .. }) + /// )); + /// ``` + pub fn lookup_entries(&self, locale: &Locale, kind: Kind) -> Result> { + self.check_locale(locale)?; + match kind { + Kind::Action | Kind::Value | Kind::Event => Ok(self + .entries_of(kind) + .map(|entry| self.builtin_match(entry, locale)) + .collect()), + Kind::Enum => Ok(self + .enum_domains() + .map(|domain| self.enum_domain_match(domain, locale)) + .collect()), + Kind::Structural | Kind::Operator | Kind::Setting => Err(WorkshopError::unsupported( + format!( + "listing kind '{}' is not supported: \ + the listable kinds are action, value, event, and enum", + kind.as_str() + ), + None, + )), + } + } + + fn check_locale(&self, locale: &Locale) -> Result<()> { + if self.supports(locale) { + return Ok(()); + } + Err(WorkshopError::unsupported( + format!( + "lookup for locale '{locale}' is not supported: \ + the catalog does not declare it" + ), + None, + )) + } + + fn builtin_match(&self, entry: &CatalogEntry, locale: &Locale) -> LookupMatch { + LookupMatch::Builtin { + kind: entry.kind, + id: entry.id.clone(), + display_name: entry.spelling(locale).map(String::from), + signature: signature(self, entry, locale), + } + } + + fn enum_domain_match(&self, domain: &EnumDomain, locale: &Locale) -> LookupMatch { + LookupMatch::EnumDomain { + domain: domain.domain.clone(), + display_name: domain.spelling(locale).map(String::from), + members: self.enum_members(&domain.domain, locale), + } + } + fn within_enum_domain( &self, locale: &Locale, diff --git a/crates/workshop-rs/tests/lookup.rs b/crates/workshop-rs/tests/lookup.rs index 7b09b733..795bc984 100644 --- a/crates/workshop-rs/tests/lookup.rs +++ b/crates/workshop-rs/tests/lookup.rs @@ -669,3 +669,137 @@ fn scoped_lookup_reports_an_unknown_scope() { "unknown scope is an Unknown diagnostic: {error:?}" ); } + +#[test] +fn listing_entries_keeps_catalog_order_for_each_builtin_kind() { + let catalog = catalog(); + for kind in [Kind::Action, Kind::Value, Kind::Event] { + let listed = catalog + .lookup_entries(&en(), kind) + .expect("a builtin kind is listable"); + let ids: Vec<&str> = listed + .iter() + .map(|entry| match entry { + LookupMatch::Builtin { + kind: listed_kind, + id, + .. + } => { + assert_eq!(*listed_kind, kind); + id.as_str() + } + other => panic!("a builtin kind lists builtins: {other:?}"), + }) + .collect(); + let expected: Vec<&str> = catalog + .entries_of(kind) + .map(|entry| entry.id.as_str()) + .collect(); + assert_eq!(ids, expected, "{kind:?} listing keeps catalog order"); + let unique: std::collections::HashSet<&str> = ids.iter().copied().collect(); + assert_eq!( + unique.len(), + ids.len(), + "{kind:?} listing has no duplicates" + ); + } +} + +#[test] +fn listing_entries_keeps_catalog_order_for_enum_domains() { + let catalog = catalog(); + let listed = catalog + .lookup_entries(&en(), Kind::Enum) + .expect("enum domains are listable"); + let domains: Vec<&str> = listed + .iter() + .map(|entry| match entry { + LookupMatch::EnumDomain { domain, .. } => domain.as_str(), + other => panic!("Kind::Enum lists enum domains: {other:?}"), + }) + .collect(); + let expected: Vec<&str> = catalog + .enum_domains() + .map(|domain| domain.domain.as_str()) + .collect(); + assert_eq!(domains, expected); +} + +#[test] +fn listed_entries_are_the_matches_lookup_returns() { + let catalog = catalog(); + for locale in [en(), zh()] { + for kind in [Kind::Action, Kind::Value, Kind::Event] { + for listed in catalog + .lookup_entries(&locale, kind) + .expect("a builtin kind is listable") + { + let LookupMatch::Builtin { + kind: listed_kind, + id, + .. + } = &listed + else { + panic!("a builtin kind lists builtins: {listed:?}"); + }; + // A query of the exact id can return another Builtin first: + // `modifyGlobalVariable` is declared under both Structural + // and Action. Compare against the Builtin match of the same + // kind and id rather than the top-ranked match. + let from_lookup = catalog + .lookup(&locale, id) + .expect("lookup answers") + .into_iter() + .find(|entry| { + matches!(entry, LookupMatch::Builtin { + kind: found_kind, + id: found, + .. + } if found == id && found_kind == listed_kind) + }) + .unwrap_or_else(|| panic!("lookup finds {listed_kind:?} '{id}'")); + assert_eq!(listed, from_lookup, "{locale} {kind:?} '{id}'"); + } + } + for listed in catalog + .lookup_entries(&locale, Kind::Enum) + .expect("enum domains are listable") + { + let LookupMatch::EnumDomain { domain, .. } = &listed else { + panic!("Kind::Enum lists enum domains: {listed:?}"); + }; + let from_lookup = catalog + .lookup(&locale, domain) + .expect("lookup answers") + .into_iter() + .find(|entry| { + matches!(entry, LookupMatch::EnumDomain { domain: found, .. } if found == domain) + }) + .unwrap_or_else(|| panic!("lookup finds domain '{domain}'")); + assert_eq!(listed, from_lookup, "{locale} domain '{domain}'"); + } + } +} + +#[test] +fn listing_entries_rejects_undeclared_locales_and_unlistable_kinds() { + let catalog = catalog(); + for kind in [Kind::Action, Kind::Value, Kind::Event, Kind::Enum] { + let error = catalog + .lookup_entries(&Locale::new("es-CL"), kind) + .expect_err("an undeclared locale is unsupported"); + assert!( + matches!(error, WorkshopError::Unsupported { .. }), + "{kind:?}: {error:?}" + ); + } + for kind in [Kind::Structural, Kind::Operator, Kind::Setting] { + let error = catalog + .lookup_entries(&en(), kind) + .expect_err("a kind with no listing surface is unsupported"); + assert!( + matches!(error, WorkshopError::Unsupported { .. }), + "{kind:?}: {error:?}" + ); + } +} diff --git a/docs/lookup.md b/docs/lookup.md index d881ebcd..12861bd2 100644 --- a/docs/lookup.md +++ b/docs/lookup.md @@ -77,6 +77,34 @@ the next path segment below a settings prefix (`heroes.`, naming no known scope is an explicit `WorkshopError::Unknown` with `kind: "lookup scope"`. +## Listing entries + +`Catalog::lookup_entries` enumerates the whole listable vocabulary of a +kind under `locale` — no query, matching, or ranking; the result keeps +catalog order. Each element is the same `LookupMatch` a `lookup` query +builds for that entry, signature included, so a consumer paging through +the list sees the entry a query would have returned. + +```rust +use workshop_rs::catalog::{Catalog, Kind, Locale}; +use workshop_rs::lookup::LookupMatch; + +let catalog = Catalog::builtin()?; +let values = catalog.lookup_entries(&Locale::new("en-US"), Kind::Value)?; +assert!(values.iter().all(|m| matches!(m, LookupMatch::Builtin { kind: Kind::Value, .. }))); +# Ok::<(), workshop_rs::WorkshopError>(()) +``` + +`Kind::Action`, `Kind::Value`, and `Kind::Event` list +`LookupMatch::Builtin` matches in `entries_of` order; `Kind::Enum` +lists one `LookupMatch::EnumDomain` per declared domain in +`enum_domains` order. A domain's members and a callable's parameters +are scoped children that already list through `lookup_within`. +Structural keywords, operators, and settings have no listing surface — +naming such a kind is a `WorkshopError::Unsupported`, as is a locale +the catalog does not declare. The result is the complete vocabulary of +the kind — paging, cursors, and presentation belong to the consumer. + ## Signature form `Signature::text` renders the call syntax in the requested locale: parameters