Add autocomplete to math expression

Add min, max, randomchoice
This commit is contained in:
pythongosssss
2023-10-01 12:59:45 +01:00
parent bb2e653c1f
commit b477f2d7f3
3 changed files with 153 additions and 31 deletions
+43 -8
View File
@@ -15,25 +15,53 @@ operators = {
ast.Mod: op.mod
}
# TODO: restructure args to provide more info, generate hint based on args to save duplication
functions = {
"round": {
"args": (1, 2),
"call": lambda a, b = None: round(a, b)
"call": lambda a, b = None: round(a, b),
"hint": "number, dp? = 0"
},
"ceil": {
"args": (1, 1),
"call": lambda a: math.ceil(a)
"call": lambda a: math.ceil(a),
"hint": "number"
},
"floor": {
"args": (1, 1),
"call": lambda a: math.floor(a)
"call": lambda a: math.floor(a),
"hint": "number"
},
"min": {
"args": (2, None),
"call": lambda *args: min(*args),
"hint": "...numbers"
},
"max": {
"args": (2, None),
"call": lambda *args: max(*args),
"hint": "...numbers"
},
"randomint": {
"args": (2, 2),
"call": lambda a, b: random.randint(a, b)
}
"call": lambda a, b: random.randint(a, b),
"hint": "min, max"
},
"randomchoice": {
"args": (2, None),
"call": lambda *args: random.choice(args),
"hint": "...numbers"
},
}
autocompleteWords = list({
"text": x,
"value": f"{x}()",
"showValue": False,
"hint": f"{functions[x]['hint']}",
"caretOffset": -1
} for x in functions.keys())
class MathExpression:
@@ -41,7 +69,10 @@ class MathExpression:
def INPUT_TYPES(cls):
return {
"required": {
"expression": ("STRING", {"multiline": True, "dynamicPrompts": False, "pysssss.autocomplete": False}),
"expression": ("STRING", {"multiline": True, "dynamicPrompts": False, "pysssss.autocomplete": {
"words": autocompleteWords,
"separator": ""
}}),
},
"optional": {
"a": ("INT,FLOAT,IMAGE,LATENT", ),
@@ -131,9 +162,13 @@ class MathExpression:
if node.func.id in functions:
fn = functions[node.func.id]
l = len(node.args)
if l < fn["args"][0] or l > fn["args"][1]:
if l < fn["args"][0] or (fn["args"][1] is not None and l > fn["args"][1]):
if fn["args"][1] is None:
toErr = " or more"
else:
toErr = f" to {fn['args'][1]}"
raise SyntaxError(
f"Invalid function call: {node.func.id} requires {fn['args'][0]} to {fn['args'][1]} arguments")
f"Invalid function call: {node.func.id} requires {fn['args'][0]}{toErr} arguments")
args = []
for arg in node.args:
args.append(eval_expr(arg))
+50 -7
View File
@@ -275,17 +275,43 @@ app.registerExtension({
ComfyWidgets.STRING = function (node, inputName, inputData) {
const r = STRING.apply(this, arguments);
if (inputData[1]?.multiline && inputData[1]?.["pysssss.autocomplete"] !== false) {
if (inputData[1]?.multiline) {
// Disabled on this input
const config = inputData[1]?.["pysssss.autocomplete"];
if (config === false) return r;
// In list of widgets to skip
const id = `${node.comfyClass}.${inputName}`;
if (!SKIP_WIDGETS.has(id)) {
new TextAreaAutoComplete(r.widget.inputEl);
if (SKIP_WIDGETS.has(id)) return r;
let words;
let separator;
if (typeof config === "object") {
separator = config.separator;
words = {};
if (config.words) {
// Custom wordlist, this will have been registered on setup
Object.assign(words, TextAreaAutoComplete.groups[node.comfyClass + "." + inputName] ?? {});
}
for (const item of config.groups ?? []) {
if (item === "*") {
// This widget wants all global words included
Object.assign(words, TextAreaAutoComplete.globalWords);
} else {
// This widget wants a specific group included
Object.assign(words, TextAreaAutoComplete.groups[item] ?? {});
}
}
}
new TextAreaAutoComplete(r.widget.inputEl, words, separator);
}
return r;
};
TextAreaAutoComplete.separator = localStorage.getItem(id + ".AutoSeparate") ?? ", ";
TextAreaAutoComplete.globalSeparator = localStorage.getItem(id + ".AutoSeparate") ?? ", ";
app.ui.settings.addSetting({
id,
name: "🐍 Text Autocomplete",
@@ -332,11 +358,11 @@ app.registerExtension({
$el("input", {
id: id.replaceAll(".", "-"),
type: "checkbox",
checked: !!TextAreaAutoComplete.separator,
checked: !!TextAreaAutoComplete.globalSeparator,
onchange: (event) => {
const checked = !!event.target.checked;
TextAreaAutoComplete.separator = checked ? ", " : "";
localStorage.setItem(id + ".AutoSeparate", TextAreaAutoComplete.separator);
TextAreaAutoComplete.globalSeparator = checked ? ", " : "";
localStorage.setItem(id + ".AutoSeparate", TextAreaAutoComplete.globalSeparator);
},
}),
]
@@ -358,4 +384,21 @@ app.registerExtension({
},
});
},
beforeRegisterNodeDef(_, def) {
// Process each input to see if there is a custom word list for
// { input: { required: { something: ["STRING", { "pysssss.autocomplete": ["groupid", ["custom", "words"] ] }] } } }
const inputs = { ...def.input?.required, ...def.input?.optional };
for (const input in inputs) {
const config = inputs[input][1]?.["pysssss.autocomplete"];
if (!config) continue;
if (typeof config === "object" && config.words) {
const words = {};
for (const text of config.words || []) {
const obj = typeof text === "string" ? { text } : text;
words[obj.text] = obj;
}
TextAreaAutoComplete.updateWords(def.name + "." + input, words, false);
}
}
},
});
+60 -16
View File
@@ -284,14 +284,14 @@ class TextAreaCaretHelper {
return this.el.value.substring(this.el.selectionEnd);
}
insertAtCursor(value, offset) {
insertAtCursor(value, offset, finalOffset) {
if (this.el.selectionStart != null) {
const startPos = this.el.selectionStart;
const endPos = this.el.selectionEnd;
this.el.value =
this.el.value.substring(0, startPos + offset) + value + this.el.value.substring(endPos, this.el.value.length);
this.el.selectionEnd = this.el.selectionStart = startPos + value.length + offset;
this.el.selectionEnd = this.el.selectionStart = startPos + value.length + offset + finalOffset;
} else {
this.el.value += value;
}
@@ -300,25 +300,51 @@ class TextAreaCaretHelper {
/*********************/
/**
* @typedef {{
* text: string,
* priority?: number,
* info?: Function,
* hint?: string,
* showValue?: boolean,
* caretOffset?: number
* }} AutoCompleteEntry
*/
export class TextAreaAutoComplete {
static separator = "";
static globalSeparator = "";
static enabled = true;
/** @type {Record<string, Record<string, AutoCompleteEntry>>} */
static groups = {};
/** @type {Record<string, {text: string, priority?: number; info?: Function}>} */
static words = {};
/** @type {Set<string>} */
static globalGroups = new Set();
/** @type {Record<string, AutoCompleteEntry>} */
static globalWords = {};
/** @type {HTMLTextAreaElement} */
el;
/** @type {Record<string, AutoCompleteEntry>} */
overrideWords;
overrideSeparator = "";
get words() {
return this.overrideWords ?? TextAreaAutoComplete.globalWords;
}
get separator() {
return this.overrideSeparator ?? TextAreaAutoComplete.globalSeparator;
}
/**
* @param {HTMLTextAreaElement} el
*/
constructor(el) {
constructor(el, words = null, separator = null) {
this.el = el;
this.helper = new TextAreaCaretHelper(el);
this.dropdown = $el("div.pysssss-autocomplete");
this.overrideWords = words;
this.overrideSeparator = separator;
this.#setup();
}
@@ -424,7 +450,7 @@ export class TextAreaAutoComplete {
const priorityMatches = [];
const prefixMatches = [];
const includesMatches = [];
for (const word of Object.keys(TextAreaAutoComplete.words)) {
for (const word of Object.keys(this.words)) {
const lowerWord = word.toLocaleLowerCase();
if (lowerWord === term) {
// Dont include exact matches
@@ -437,7 +463,7 @@ export class TextAreaAutoComplete {
continue;
}
const wordInfo = TextAreaAutoComplete.words[word];
const wordInfo = this.words[word];
if (wordInfo.priority) {
priorityMatches.push({ pos, wordInfo });
} else if (pos) {
@@ -499,6 +525,14 @@ export class TextAreaAutoComplete {
}),
];
if (wordInfo.hint) {
parts.push(
$el("span.pysssss-autocomplete-pill", {
textContent: wordInfo.hint,
})
);
}
if (wordInfo.priority) {
parts.push(
$el("span.pysssss-autocomplete-pill", {
@@ -507,7 +541,7 @@ export class TextAreaAutoComplete {
);
}
if (wordInfo.value && wordInfo.text !== wordInfo.value) {
if (wordInfo.value && wordInfo.text !== wordInfo.value && wordInfo.showValue !== false) {
parts.push(
$el("span.pysssss-autocomplete-pill", {
textContent: wordInfo.value,
@@ -534,8 +568,9 @@ export class TextAreaAutoComplete {
onclick: () => {
this.el.focus();
this.helper.insertAtCursor(
(wordInfo.value ?? wordInfo.text) + TextAreaAutoComplete.separator,
-before.length
(wordInfo.value ?? wordInfo.text) + this.separator,
-before.length,
wordInfo.caretOffset
);
setTimeout(() => {
this.#update();
@@ -575,15 +610,24 @@ export class TextAreaAutoComplete {
this.dropdown.remove();
}
static updateWords(id, words) {
static updateWords(id, words, addGlobal = true) {
const isUpdate = id in TextAreaAutoComplete.groups;
TextAreaAutoComplete.groups[id] = words;
if (addGlobal) {
TextAreaAutoComplete.globalGroups.add(id);
}
if (isUpdate) {
// Remerge all words
TextAreaAutoComplete.words = Object.assign({}, ...Object.values(TextAreaAutoComplete.groups));
} else {
TextAreaAutoComplete.globalWords = Object.assign(
{},
...Object.keys(TextAreaAutoComplete.groups)
.filter((k) => TextAreaAutoComplete.globalGroups.has(k))
.map((k) => TextAreaAutoComplete.groups[k])
);
} else if (addGlobal) {
// Just insert the new words
Object.assign(TextAreaAutoComplete.words, words);
Object.assign(TextAreaAutoComplete.globalWords, words);
}
}
}