Add autocomplete to math expression
Add min, max, randomchoice
This commit is contained in:
+43
-8
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user