fix(schematic): rewrite component lookup to handle single-line files
The previous line-based parser (split("\n") + line-by-line regex)
failed when the MCP server itself generates .kicad_sch files as a
single line. The new implementation works directly on the raw content
string using parenthesis-depth matching to find symbol blocks,
making it independent of formatting/whitespace.
This commit is contained in:
@@ -762,61 +762,66 @@ class KiCADInterface:
|
|||||||
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
|
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
|
||||||
|
|
||||||
with open(sch_file, "r", encoding="utf-8") as f:
|
with open(sch_file, "r", encoding="utf-8") as f:
|
||||||
lines = f.read().split("\n")
|
content = f.read()
|
||||||
|
|
||||||
# Find lib_symbols range to skip
|
def find_matching_paren(s, start):
|
||||||
lib_sym_start, lib_sym_end = None, None
|
"""Find the position of the closing paren matching the opening paren at start."""
|
||||||
depth = 0
|
depth = 0
|
||||||
for i, line in enumerate(lines):
|
i = start
|
||||||
if "(lib_symbols" in line and lib_sym_start is None:
|
while i < len(s):
|
||||||
lib_sym_start = i
|
if s[i] == '(':
|
||||||
depth = sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
|
depth += 1
|
||||||
elif lib_sym_start is not None and lib_sym_end is None:
|
elif s[i] == ')':
|
||||||
depth += sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
|
depth -= 1
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
lib_sym_end = i
|
return i
|
||||||
break
|
i += 1
|
||||||
|
return -1
|
||||||
|
|
||||||
# Find the placed symbol block
|
# Skip lib_symbols section
|
||||||
|
lib_sym_pos = content.find("(lib_symbols")
|
||||||
|
lib_sym_end = find_matching_paren(content, lib_sym_pos) if lib_sym_pos >= 0 else -1
|
||||||
|
|
||||||
|
# Find placed symbol blocks that match the reference
|
||||||
|
# Search for (symbol (lib_id "...") ... (property "Reference" "<ref>" ...) ...)
|
||||||
block_start = block_end = None
|
block_start = block_end = None
|
||||||
i = 0
|
search_start = 0
|
||||||
while i < len(lines):
|
pattern = re.compile(r'\(symbol\s+\(lib_id\s+"')
|
||||||
if lib_sym_start is not None and lib_sym_end is not None:
|
while True:
|
||||||
if lib_sym_start <= i <= lib_sym_end:
|
m = pattern.search(content, search_start)
|
||||||
i += 1
|
if not m:
|
||||||
continue
|
|
||||||
if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", lines[i]):
|
|
||||||
b_start = i
|
|
||||||
b_depth = sum(1 for c in lines[i] if c == "(") - sum(1 for c in lines[i] if c == ")")
|
|
||||||
j = i + 1
|
|
||||||
while j < len(lines) and b_depth > 0:
|
|
||||||
b_depth += sum(1 for c in lines[j] if c == "(") - sum(1 for c in lines[j] if c == ")")
|
|
||||||
j += 1
|
|
||||||
b_end = j - 1
|
|
||||||
block_text = "\n".join(lines[b_start:b_end + 1])
|
|
||||||
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
|
|
||||||
block_start, block_end = b_start, b_end
|
|
||||||
break
|
break
|
||||||
i = b_end + 1
|
pos = m.start()
|
||||||
|
# Skip if inside lib_symbols section
|
||||||
|
if lib_sym_pos >= 0 and lib_sym_pos <= pos <= lib_sym_end:
|
||||||
|
search_start = lib_sym_end + 1
|
||||||
continue
|
continue
|
||||||
i += 1
|
end = find_matching_paren(content, pos)
|
||||||
|
if end < 0:
|
||||||
|
search_start = pos + 1
|
||||||
|
continue
|
||||||
|
block_text = content[pos:end + 1]
|
||||||
|
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
|
||||||
|
block_start, block_end = pos, end
|
||||||
|
break
|
||||||
|
search_start = end + 1
|
||||||
|
|
||||||
if block_start is None:
|
if block_start is None:
|
||||||
return {"success": False, "message": f"Component '{reference}' not found in schematic"}
|
return {"success": False, "message": f"Component '{reference}' not found in schematic"}
|
||||||
|
|
||||||
# Apply in-place property updates within the block
|
# Apply property replacements within the found block
|
||||||
for k in range(block_start, block_end + 1):
|
block_text = content[block_start:block_end + 1]
|
||||||
line = lines[k]
|
if new_footprint is not None:
|
||||||
if new_footprint is not None and re.match(r'\s*\(property\s+"Footprint"\s+"', line):
|
block_text = re.sub(r'(\(property\s+"Footprint"\s+)"[^"]*"', rf'\1"{new_footprint}"', block_text)
|
||||||
line = re.sub(r'(\(property\s+"Footprint"\s+)"[^"]*"', rf'\1"{new_footprint}"', line)
|
if new_value is not None:
|
||||||
if new_value is not None and re.match(r'\s*\(property\s+"Value"\s+"', line):
|
block_text = re.sub(r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', block_text)
|
||||||
line = re.sub(r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', line)
|
if new_reference is not None:
|
||||||
if new_reference is not None and re.match(r'\s*\(property\s+"Reference"\s+"', line):
|
block_text = re.sub(r'(\(property\s+"Reference"\s+)"[^"]*"', rf'\1"{new_reference}"', block_text)
|
||||||
line = re.sub(r'(\(property\s+"Reference"\s+)"[^"]*"', rf'\1"{new_reference}"', line)
|
|
||||||
lines[k] = line
|
content = content[:block_start] + block_text + content[block_end + 1:]
|
||||||
|
|
||||||
with open(sch_file, "w", encoding="utf-8") as f:
|
with open(sch_file, "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(lines))
|
f.write(content)
|
||||||
|
|
||||||
changes = {k: v for k, v in {"footprint": new_footprint, "value": new_value, "reference": new_reference}.items() if v is not None}
|
changes = {k: v for k, v in {"footprint": new_footprint, "value": new_value, "reference": new_reference}.items() if v is not None}
|
||||||
logger.info(f"Edited schematic component {reference}: {changes}")
|
logger.info(f"Edited schematic component {reference}: {changes}")
|
||||||
|
|||||||
Reference in New Issue
Block a user