feat: numbered gutter in the Arguments editor

The one-arg-per-line model read as odd text wrapping — a path on its
own line looked like a wrapped continuation of the previous argument.
A line-number gutter makes each argument visibly its own item.
ArgsEdit also owns the NoWrap setting now.
This commit is contained in:
AJ
2026-07-02 00:05:26 -04:00
parent d2f7ce112c
commit d6ce4a0fb0
+73 -6
View File
@@ -13,8 +13,8 @@ from __future__ import annotations
import sys
from pathlib import Path
from PySide6.QtCore import QSettings, Qt, QThread, QTimer, Signal
from PySide6.QtGui import QAction, QColor, QGuiApplication
from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, Signal
from PySide6.QtGui import QAction, QColor, QGuiApplication, QPainter
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
@@ -361,14 +361,11 @@ class ServerEditor(QFrame):
self.command.textChanged.connect(self._emit)
v.addWidget(self.command)
v.addWidget(self._lbl("Arguments (one per line)"))
self.args = QPlainTextEdit()
self.args = ArgsEdit()
self.args.setPlaceholderText(
"-y\n@modelcontextprotocol/server-filesystem\n/Users/you/Documents"
)
self.args.setMinimumHeight(70)
# One argument per line means wrapping is actively misleading — a long
# path that soft-wraps looks like two separate args. Scroll instead.
self.args.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.args.textChanged.connect(self._emit)
v.addWidget(self.args, 1)
v.addWidget(self._lbl("Environment variables"))
@@ -565,6 +562,76 @@ class ServerEditor(QFrame):
self.dep_label.setStyleSheet(f"color: {color};")
# --------------------------------------------------------------------------- #
# Arguments editor: one line = one argument, with a numbered gutter so that
# model is visible (a long path on its own line reads as "arg 2", not as a
# wrapped continuation of arg 1).
# --------------------------------------------------------------------------- #
class _ArgsGutter(QWidget):
def __init__(self, editor: ArgsEdit):
super().__init__(editor)
self._editor = editor
def sizeHint(self):
return QSize(self._editor.gutter_width(), 0)
def paintEvent(self, event):
self._editor.gutter_paint(event)
class ArgsEdit(QPlainTextEdit):
def __init__(self):
super().__init__()
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self._gutter = _ArgsGutter(self)
self.blockCountChanged.connect(self._update_gutter_width)
self.updateRequest.connect(self._on_update_request)
self._update_gutter_width()
def gutter_width(self) -> int:
digits = max(1, len(str(self.blockCount())))
return 14 + self.fontMetrics().horizontalAdvance("9") * digits
def _update_gutter_width(self, *_):
self.setViewportMargins(self.gutter_width(), 0, 0, 0)
def _on_update_request(self, rect, dy):
if dy:
self._gutter.scroll(0, dy)
else:
self._gutter.update(0, rect.y(), self._gutter.width(), rect.height())
if rect.contains(self.viewport().rect()):
self._update_gutter_width()
def resizeEvent(self, e):
super().resizeEvent(e)
cr = self.contentsRect()
self._gutter.setGeometry(QRect(cr.left(), cr.top(), self.gutter_width(), cr.height()))
def gutter_paint(self, event):
painter = QPainter(self._gutter)
painter.setPen(QColor(MUTED))
fm = self.fontMetrics()
block = self.firstVisibleBlock()
n = block.blockNumber()
top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
bottom = top + round(self.blockBoundingRect(block).height())
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
painter.drawText(
0,
top,
self._gutter.width() - 8,
fm.height(),
Qt.AlignmentFlag.AlignRight,
str(n + 1),
)
block = block.next()
top = bottom
bottom = top + round(self.blockBoundingRect(block).height())
n += 1
# --------------------------------------------------------------------------- #
# Paste-JSON dialog
# --------------------------------------------------------------------------- #