///|
struct Ensemble {
namespace_name : String
mut mapping : Array[(String, Array[TclValue])]
mut parameters : Array[String]
mut prefixes : Bool
mut subcommands : Array[String]
mut unknown : Array[TclValue]
mut option_text : Map[String, TclValue]
} derive(Debug)
///|
fn Ensemble::option(
self : Ensemble,
option : String,
) -> TclValue raise TclError {
match
select_keyword(option, [
"-namespace", "-map", "-parameters", "-prefixes", "-subcommands", "-unknown",
]) {
"-namespace" => text_value(self.namespace_name)
"-prefixes" => text_value(boolean_text(self.prefixes))
key => self.option_text.get(key).unwrap_or(text_value(""))
}
}
///|
fn Interpreter::configure_ensemble(
self : Interpreter,
ensemble : Ensemble,
options : Array[TclValue],
) -> Unit raise TclError {
if options.length() % 2 != 0 {
raise Invalid("ensemble option value required")
}
// Tcl validates all options before changing an existing ensemble.
let candidate = {
..ensemble,
option_text: Map::from_iter(ensemble.option_text.iter()),
}
for i = 0; i < options.length(); i = i + 2 {
let object = options[i + 1]
let value = object.text
let key = select_keyword(options[i].text, [
"-namespace", "-map", "-parameters", "-prefixes", "-subcommands", "-unknown",
])
candidate.option_text[key] = object
match key {
"-map" => {
let parts = object.as_list()
if parts.length() % 2 != 0 {
raise Invalid("ensemble map must be dictionary")
}
let mapping : Array[(String, Array[TclValue])] = []
let mut changed = false
for j = 0; j < parts.length(); j = j + 2 {
let words = parts[j + 1].as_list()
if words.is_empty() {
raise Invalid("ensemble command prefix must not be empty")
}
if !words[0].text.has_prefix("::") {
changed = true
words[0] = text_value(
command_name(self.frame.namespace_name, words[0].text),
)
}
let mut previous = -1
for k in 0.. candidate.parameters = parse_list(value)
"-prefixes" => candidate.prefixes = boolean(value)
"-subcommands" => candidate.subcommands = parse_list(value)
"-unknown" => candidate.unknown = object.as_list()
_ => raise Invalid("unknown or read-only ensemble option")
}
}
ensemble.option_text = candidate.option_text
ensemble.mapping = candidate.mapping
ensemble.parameters = candidate.parameters
ensemble.prefixes = candidate.prefixes
ensemble.subcommands = candidate.subcommands
ensemble.unknown = candidate.unknown
}
///|
fn Interpreter::ensemble_command(
self : Interpreter,
input : Array[TclValue],
) -> TclValue raise TclError {
let args = input.map(v => v.text)
let n = args.length()
if n < 3 {
raise Invalid("namespace ensemble arity")
}
let text = match select_keyword(args[2], ["create", "exists", "configure"]) {
"create" => {
if n % 2 != 1 {
raise Invalid("ensemble option value required")
}
let mut name = self.frame.namespace_name
let options = []
for i = 3; i < n; i = i + 2 {
let option = select_keyword(args[i], [
"-command", "-map", "-parameters", "-prefixes", "-subcommands", "-unknown",
])
if option == "-command" {
name = command_name(self.frame.namespace_name, args[i + 1])
} else {
options.push(text_value(option))
options.push(input[i + 1])
}
}
let ensemble = Ensemble::{
namespace_name: self.frame.namespace_name,
mapping: [],
parameters: [],
prefixes: true,
subcommands: [],
unknown: [],
option_text: Map([]),
}
self.configure_ensemble(ensemble, options)
let parts = command_parent(name)
.split("::")
.filter(s => !s.is_empty())
.to_array()
let mut prefix = "::"
for part in parts {
prefix = qualified_name(prefix, part.to_owned())
self.state.namespaces[prefix] = true
}
self.define_command(name, EnsembleCommand(ensemble))
name
}
"exists" => {
if n != 4 {
raise Invalid("namespace ensemble exists arity")
}
boolean_text(
match self.find_command(args[3]) {
Some(command) => command.origin().body is EnsembleCommand(_)
None => false
},
)
}
"configure" => {
if n < 4 {
raise Invalid("namespace ensemble configure arity")
}
let ensemble = match self.find_command(args[3]) {
Some(command) =>
match command.origin().body {
EnsembleCommand(ensemble) => ensemble
_ => raise Invalid("command is not an ensemble")
}
None => raise Invalid("unknown ensemble")
}
if n == 4 {
let values = []
for
option in [
"-map", "-namespace", "-parameters", "-prefixes", "-subcommands", "-unknown",
] {
values.push(text_value(option))
values.push(ensemble.option(option))
}
return list_value(values)
} else if n == 5 {
return ensemble.option(args[4])
} else {
self.configure_ensemble(ensemble, input[4:].to_owned())
""
}
}
_ => raise Invalid("unsupported ensemble subcommand")
}
text_value(text)
}
///|
fn Interpreter::ensemble_prefix(
self : Interpreter,
ensemble : Ensemble,
name : String,
) -> Array[TclValue]? raise TclError {
let candidates = if !ensemble.subcommands.is_empty() {
ensemble.subcommands
} else if !ensemble.mapping.is_empty() {
ensemble.mapping.map(pair => pair.0)
} else {
self
.exported_commands(ensemble.namespace_name)
.map(c => namespace_tail(c.name))
}
let mut selected : String? = None
if candidates.contains(name) {
selected = Some(name)
} else if ensemble.prefixes {
for candidate in candidates {
if candidate.has_prefix(name) {
match selected {
Some(old) => if old != candidate { return None }
None => selected = Some(candidate)
}
}
}
}
match selected {
None => None
Some(key) => {
for (mapped, words) in ensemble.mapping {
if mapped == key {
return Some(words)
}
}
Some([text_value(command_name(ensemble.namespace_name, key))])
}
}
}
///|
fn Interpreter::invoke_ensemble(
self : Interpreter,
command : Command,
ensemble : Ensemble,
input : Array[TclValue],
depth : Int,
discard_result? : Bool = false,
) -> TclValue raise TclError {
let mut index = 1 + ensemble.parameters.length()
if input.length() <= index {
raise Invalid("ensemble subcommand required")
}
let mut prefix = self.ensemble_prefix(ensemble, input[index].text)
if prefix is None && !ensemble.unknown.is_empty() {
let value = self.command_value(
ensemble.unknown + [text_value(command.name)] + input[1:].to_owned(),
depth + 1,
) catch {
Invalid(message) => raise Invalid(message)
Signal(result) if result.actual_code() == 1 => raise Signal(result)
_ => raise Invalid("invalid completion from ensemble unknown handler")
}
let still_live = match self.state.commands.get(command.name) {
Some(current) =>
physical_equal(current, command) &&
(match current.body {
EnsembleCommand(active) => physical_equal(active, ensemble)
_ => false
})
None => false
}
if !still_live {
raise Invalid("unknown handler deleted its ensemble")
}
index = 1 + ensemble.parameters.length()
let words = value.as_list()
if !words.is_empty() {
// A supplied rewrite bypasses reparsing. Tcl retains the original
// invocation word count even when this callback changed parameters.
// Clamp the removed subcommand position to the available invocation.
if index >= input.length() {
index = input.length() - 1
}
prefix = Some(words)
} else {
if input.length() <= index {
raise Invalid("ensemble subcommand required")
}
prefix = self.ensemble_prefix(ensemble, input[index].text)
}
}
let words = match prefix {
Some(words) => words
None => raise Invalid("unknown or ambiguous ensemble subcommand")
}
self.command_value(
words + input[1:index].to_owned() + input[index + 1:].to_owned(),
depth + 1,
discard_result~,
)
}