Compare commits

...

2 Commits

Author SHA1 Message Date
Kelly Yang
b084fc8cff fix: remove unused Enclosure type export 2026-04-15 22:16:33 -07:00
Kelly Yang
21e7163e9e test: add unit tests for editAttention parsing logic
Extract incrementWeight, findNearestEnclosure, and addWeightToParentheses
as exported module-level functions and cover them with unit tests.

Closes #11107
2026-04-15 22:16:12 -07:00
2 changed files with 158 additions and 67 deletions

View File

@@ -0,0 +1,102 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/scripts/app', () => ({
app: {
registerExtension: vi.fn(),
ui: { settings: { addSetting: vi.fn() } }
}
}))
import {
addWeightToParentheses,
findNearestEnclosure,
incrementWeight
} from './editAttention'
describe('incrementWeight', () => {
it('increments a weight by the given delta', () => {
expect(incrementWeight('1.0', 0.05)).toBe('1.05')
})
it('decrements a weight by the given delta', () => {
expect(incrementWeight('1.05', -0.05)).toBe('1')
})
it('returns the original string when weight is not a number', () => {
expect(incrementWeight('abc', 0.05)).toBe('abc')
})
it('rounds correctly and avoids floating point accumulation', () => {
expect(incrementWeight('1.1', 0.1)).toBe('1.2')
})
it('can produce a weight of zero', () => {
expect(incrementWeight('0.05', -0.05)).toBe('0')
})
it('produces negative weights', () => {
expect(incrementWeight('0.0', -0.05)).toBe('-0.05')
})
})
describe('findNearestEnclosure', () => {
it('returns start and end of a simple parenthesized expression', () => {
expect(findNearestEnclosure('(cat)', 2)).toEqual({ start: 1, end: 4 })
})
it('returns null when there are no parentheses', () => {
expect(findNearestEnclosure('cat dog', 3)).toBeNull()
})
it('returns null when cursor is outside any enclosure', () => {
expect(findNearestEnclosure('(cat) dog', 7)).toBeNull()
})
it('finds the inner enclosure when cursor is on nested content', () => {
expect(findNearestEnclosure('(outer (inner) end)', 9)).toEqual({
start: 8,
end: 13
})
})
it('finds the outer enclosure when cursor is on outer content', () => {
expect(findNearestEnclosure('(outer (inner) end)', 2)).toEqual({
start: 1,
end: 18
})
})
it('returns null for empty string', () => {
expect(findNearestEnclosure('', 0)).toBeNull()
})
it('returns null when opening paren has no matching closing paren', () => {
expect(findNearestEnclosure('(cat', 2)).toBeNull()
})
})
describe('addWeightToParentheses', () => {
it('adds weight 1.0 to a bare parenthesized token', () => {
expect(addWeightToParentheses('(cat)')).toBe('(cat:1.0)')
})
it('leaves a token that already has a weight unchanged', () => {
expect(addWeightToParentheses('(cat:1.5)')).toBe('(cat:1.5)')
})
it('leaves a token without parentheses unchanged', () => {
expect(addWeightToParentheses('cat')).toBe('cat')
})
it('leaves a token with scientific notation weight unchanged', () => {
expect(addWeightToParentheses('(cat:1e-3)')).toBe('(cat:1e-3)')
})
it('leaves a token with a negative weight unchanged', () => {
expect(addWeightToParentheses('(cat:-0.5)')).toBe('(cat:-0.5)')
})
it('adds weight to a multi-word parenthesized token', () => {
expect(addWeightToParentheses('(cat dog)')).toBe('(cat dog:1.0)')
})
})

View File

@@ -1,6 +1,61 @@
import { app } from '../../scripts/app'
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
type Enclosure = {
start: number
end: number
}
export function incrementWeight(weight: string, delta: number): string {
const floatWeight = parseFloat(weight)
if (isNaN(floatWeight)) return weight
const newWeight = floatWeight + delta
return String(Number(newWeight.toFixed(10)))
}
export function findNearestEnclosure(
text: string,
cursorPos: number
): Enclosure | null {
let start = cursorPos,
end = cursorPos
let openCount = 0,
closeCount = 0
while (start >= 0) {
start--
if (text[start] === '(' && openCount === closeCount) break
if (text[start] === '(') openCount++
if (text[start] === ')') closeCount++
}
if (start < 0) return null
openCount = 0
closeCount = 0
while (end < text.length) {
if (text[end] === ')' && openCount === closeCount) break
if (text[end] === '(') openCount++
if (text[end] === ')') closeCount++
end++
}
if (end === text.length) return null
return { start: start + 1, end: end }
}
export function addWeightToParentheses(text: string): string {
const parenRegex = /^\((.*)\)$/
const parenMatch = text.match(parenRegex)
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/
const floatMatch = text.match(floatRegex)
if (parenMatch && !floatMatch) {
return `(${parenMatch[1]}:1.0)`
} else {
return text
}
}
app.registerExtension({
name: 'Comfy.EditAttention',
@@ -18,65 +73,6 @@ app.registerExtension({
defaultValue: 0.05
})
function incrementWeight(weight: string, delta: number): string {
const floatWeight = parseFloat(weight)
if (isNaN(floatWeight)) return weight
const newWeight = floatWeight + delta
return String(Number(newWeight.toFixed(10)))
}
type Enclosure = {
start: number
end: number
}
function findNearestEnclosure(
text: string,
cursorPos: number
): Enclosure | null {
let start = cursorPos,
end = cursorPos
let openCount = 0,
closeCount = 0
// Find opening parenthesis before cursor
while (start >= 0) {
start--
if (text[start] === '(' && openCount === closeCount) break
if (text[start] === '(') openCount++
if (text[start] === ')') closeCount++
}
if (start < 0) return null
openCount = 0
closeCount = 0
// Find closing parenthesis after cursor
while (end < text.length) {
if (text[end] === ')' && openCount === closeCount) break
if (text[end] === '(') openCount++
if (text[end] === ')') closeCount++
end++
}
if (end === text.length) return null
return { start: start + 1, end: end }
}
function addWeightToParentheses(text: string): string {
const parenRegex = /^\((.*)\)$/
const parenMatch = text.match(parenRegex)
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/
const floatMatch = text.match(floatRegex)
if (parenMatch && !floatMatch) {
return `(${parenMatch[1]}:1.0)`
} else {
return text
}
}
function editAttention(event: KeyboardEvent) {
// @ts-expect-error Runtime narrowing not impl.
const inputField: HTMLTextAreaElement = event.composedPath()[0]
@@ -92,7 +88,6 @@ app.registerExtension({
let end = inputField.selectionEnd
let selectedText = inputField.value.substring(start, end)
// If there is no selection, attempt to find the nearest enclosure, or select the current word
if (!selectedText) {
const nearestEnclosure = findNearestEnclosure(inputField.value, start)
if (nearestEnclosure) {
@@ -100,7 +95,6 @@ app.registerExtension({
end = nearestEnclosure.end
selectedText = inputField.value.substring(start, end)
} else {
// Select the current word, find the start and end of the word
const delimiters = ' .,\\/!?%^*;:{}=-_`~()\r\n\t'
while (
@@ -122,13 +116,11 @@ app.registerExtension({
}
}
// If the selection ends with a space, remove it
if (selectedText[selectedText.length - 1] === ' ') {
selectedText = selectedText.substring(0, selectedText.length - 1)
end -= 1
}
// If there are parentheses left and right of the selection, select them
if (
inputField.value[start - 1] === '(' &&
inputField.value[end] === ')'
@@ -138,7 +130,6 @@ app.registerExtension({
selectedText = inputField.value.substring(start, end)
}
// If the selection is not enclosed in parentheses, add them
if (
selectedText[0] !== '(' ||
selectedText[selectedText.length - 1] !== ')'
@@ -146,10 +137,8 @@ app.registerExtension({
selectedText = `(${selectedText})`
}
// If the selection does not have a weight, add a weight of 1.0
selectedText = addWeightToParentheses(selectedText)
// Increment the weight
const weightDelta = event.key === 'ArrowUp' ? delta : -delta
const updatedText = selectedText.replace(
/\((.*):([+-]?\d+(?:\.\d+)?)\)/,