vibefpmgui.quick_open

Go to File (⌘P / Ctrl+P): any file of the project, or of its dependencies, found by typing part of its name, opened in the editor.

The files are those the Project files tree lists (ProjectTree.all_files): the ones ignored by git and the greyed-out ones only with its Ignored ticked. What's typed matches the file's name first (its start, then anywhere, then its letters in order: "dstat" finds demo_stats.f90), then its path ("src/dem"); the project's own files before its dependencies', then the shortest paths. Nothing typed: the files open in the editor first.

  1"""Go to File (⌘P / Ctrl+P): any file of the project, or of its dependencies,
  2found by typing part of its name, opened in the editor.
  3
  4The files are those the Project files tree lists (`ProjectTree.all_files`):
  5the ones ignored by git and the greyed-out ones only with its *Ignored*
  6ticked. What's typed matches the file's name first (its start, then
  7anywhere, then its letters in order: "dstat" finds demo_stats.f90), then
  8its path ("src/dem"); the project's own files before its dependencies',
  9then the shortest paths. Nothing typed: the files open in the editor first.
 10"""
 11
 12from __future__ import annotations
 13
 14from pathlib import Path, PurePosixPath
 15
 16from qtpy.QtCore import QEvent, QRect, QSize, Qt
 17from qtpy.QtGui import QFont, QFontMetrics, QIcon, QPalette
 18from qtpy.QtWidgets import (
 19    QApplication,
 20    QFrame,
 21    QLabel,
 22    QLineEdit,
 23    QListWidget,
 24    QListWidgetItem,
 25    QStyle,
 26    QStyledItemDelegate,
 27    QVBoxLayout,
 28    QWidget,
 29)
 30
 31from .project_tree import ProjectFile
 32
 33FILE_ROLE = Qt.UserRole + 1
 34MAX_SHOWN = 300  # files listed at once (more: type more of the name)
 35
 36
 37def _in_order(letters: str, text: str) -> bool:
 38    """Whether ``letters`` appear in ``text`` in that order (not together)."""
 39    found = 0
 40    for ch in text:
 41        if found < len(letters) and ch == letters[found]:
 42            found += 1
 43    return found == len(letters)
 44
 45
 46def score(query: str, item: ProjectFile) -> int | None:
 47    """How well a file matches what's typed (None: not at all)."""
 48    typed = "".join(query.lower().split())
 49    name, path = PurePosixPath(item.relative).name.lower(), item.relative.lower()
 50    if name.startswith(typed):
 51        return 1000
 52    if typed in name:
 53        return 800
 54    if _in_order(typed, name):
 55        return 600
 56    if typed in path:
 57        return 400
 58    if _in_order(typed, path):
 59        return 200
 60    return None
 61
 62
 63def ranked(query: str, files: list[ProjectFile], open_paths: list[str] | None = None) -> list[ProjectFile]:
 64    """The files matching ``query``, best first (see the module's description)."""
 65    open_order = {path: n for n, path in enumerate(open_paths or [])}
 66    if not query.strip():
 67        return sorted(files, key=lambda f: (open_order.get(f.path, len(open_order)), f.dependency, f.package,
 68                                            f.relative.lower()))
 69    matched = []
 70    for item in files:
 71        points = score(query, item)
 72        if points is not None:
 73            matched.append((-points, item.dependency, open_order.get(item.path, len(open_order)),
 74                            len(item.relative), item.relative.lower(), item))
 75    matched.sort(key=lambda m: m[:5])
 76    return [m[-1] for m in matched]
 77
 78
 79class _FileDelegate(QStyledItemDelegate):
 80    """A file: its icon, name (bold), folder (muted) and, for a dependency's,
 81    the package."""
 82
 83    def __init__(self, palette: QuickOpen):
 84        super().__init__(palette.list)
 85        self.palette_ = palette
 86
 87    def sizeHint(self, option, index):
 88        return QSize(option.rect.width(), QFontMetrics(option.font).height() + 10)
 89
 90    def paint(self, painter, option, index):
 91        item: ProjectFile = index.data(FILE_ROLE)
 92        style = option.widget.style() if option.widget else QApplication.style()
 93        style.drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter, option.widget)
 94        selected = bool(option.state & QStyle.State_Selected)
 95        pal = option.palette
 96        text = pal.color(QPalette.HighlightedText if selected else QPalette.Text)
 97        muted = pal.color(QPalette.HighlightedText if selected else QPalette.PlaceholderText)
 98        rect = option.rect.adjusted(8, 0, -8, 0)
 99        painter.save()
100        icon = self.palette_.icon_for(Path(item.path)) if self.palette_.icon_for else None
101        size = QFontMetrics(option.font).height()
102        x = rect.left()
103        if icon is not None and not icon.isNull():
104            icon.paint(painter, QRect(x, rect.top() + (rect.height() - size) // 2, size, size))
105            x += size + 6
106        bold = QFont(option.font)
107        bold.setBold(True)
108        name = PurePosixPath(item.relative).name
109        folder = str(PurePosixPath(item.relative).parent)
110        parts = [(name, bold, text), (f"  {folder}" if folder != "." else "", option.font, muted)]
111        if item.dependency:
112            parts.append((f"  · {item.package}", option.font, muted))
113        if item.hidden:
114            parts.append(("  · ignored", option.font, muted))
115        for part, font, color in parts:
116            if not part or x >= rect.right():
117                continue
118            painter.setFont(font)
119            painter.setPen(color)
120            metrics = QFontMetrics(font)
121            shown = metrics.elidedText(part, Qt.ElideMiddle, rect.right() - x)
122            painter.drawText(QRect(x, rect.top(), rect.right() - x, rect.height()), Qt.AlignVCenter | Qt.AlignLeft,
123                             shown)
124            x += metrics.horizontalAdvance(shown)
125        painter.restore()
126
127
128class QuickOpen(QFrame):
129    """The palette: a field to type in, the matching files under it (as the
130    Command Palette). ``opened`` is called with the file chosen."""
131
132    WIDTH = 660
133    ROWS = 14
134
135    def __init__(self, window: QWidget, opened, icon_for=None):
136        super().__init__(window, Qt.Popup)
137        self.window_ = window
138        self.opened = opened
139        self.icon_for = icon_for
140        self.files: list[ProjectFile] = []
141        self.open_paths: list[str] = []
142        self.setFrameShape(QFrame.StyledPanel)
143        self.field = QLineEdit()
144        self.field.setPlaceholderText("Type part of a file's name (letters in order: \"dstat\" finds demo_stats.f90)")
145        self.field.setClearButtonEnabled(True)
146        self.field.textChanged.connect(self._filter)
147        self.field.installEventFilter(self)
148        # Its icon, in front of what's typed (the menu item's: see set_icon).
149        self.icon_action = self.field.addAction(QIcon(), QLineEdit.LeadingPosition)
150        self.list = QListWidget()
151        self.list.setItemDelegate(_FileDelegate(self))
152        self.list.setUniformItemSizes(True)
153        self.list.setFocusPolicy(Qt.NoFocus)
154        self.list.itemClicked.connect(lambda item: self.choose(item.data(FILE_ROLE)))
155        self.note = QLabel()  # (whether ignored files are in the list, and how many more there are)
156        self.note.setStyleSheet("color: gray;")
157        layout = QVBoxLayout(self)
158        layout.setContentsMargins(6, 6, 6, 6)
159        layout.setSpacing(6)
160        layout.addWidget(self.field)
161        layout.addWidget(self.list)
162        layout.addWidget(self.note)
163        self.include_ignored = False
164
165    def set_icon(self, icon: QIcon):
166        """The icon in front of the field (the menu item's, in the theme's colors)."""
167        self.icon_action.setIcon(icon)
168
169    def open(self, files: list[ProjectFile], open_paths: list[str], include_ignored: bool):
170        """Show the palette at the top of the window, for these files."""
171        self.files, self.open_paths, self.include_ignored = files, open_paths, include_ignored
172        self.field.clear()
173        self._filter("")
174        width = min(self.WIDTH, max(320, self.window_.width() - 40))
175        row = self.list.sizeHintForRow(0) if self.list.count() else 24
176        self.resize(width, self.field.sizeHint().height() + row * self.ROWS + self.note.sizeHint().height() + 30)
177        top = self.window_.mapToGlobal(self.window_.rect().topLeft())
178        self.move(top.x() + (self.window_.width() - width) // 2, top.y() + 40)
179        self.show()
180        self.raise_()
181        self.field.setFocus()
182
183    def _filter(self, text: str):
184        self.list.clear()
185        found = ranked(text, self.files, self.open_paths)
186        for item in found[:MAX_SHOWN]:
187            row = QListWidgetItem(item.relative)
188            row.setData(FILE_ROLE, item)
189            row.setToolTip(item.path)
190            self.list.addItem(row)
191        if self.list.count():
192            self.list.setCurrentRow(0)
193        more = f"{len(found) - MAX_SHOWN} more: type more of the name. " if len(found) > MAX_SHOWN else ""
194        which = ("Files ignored by git are included" if self.include_ignored
195                 else "Files ignored by git are left out")
196        self.note.setText(f"{more}{which} (Project files → Ignored). {len(self.files)} files.")
197
198    def choose(self, item: ProjectFile):
199        self.close()
200        self.opened(item.path)
201
202    def eventFilter(self, obj, event):
203        if obj is self.field and event.type() == QEvent.KeyPress:
204            key = event.key()
205            steps = {Qt.Key_Up: -1, Qt.Key_Down: 1, Qt.Key_PageUp: -self.ROWS, Qt.Key_PageDown: self.ROWS}
206            if key in steps and self.list.count():
207                self.list.setCurrentRow(max(0, min(self.list.count() - 1, self.list.currentRow() + steps[key])))
208                return True
209            if key in (Qt.Key_Return, Qt.Key_Enter):
210                item = self.list.currentItem()
211                if item is not None:
212                    self.choose(item.data(FILE_ROLE))
213                return True
214            if key == Qt.Key_Escape:
215                self.close()
216                return True
217        return super().eventFilter(obj, event)
FILE_ROLE = 257
MAX_SHOWN = 300
def score(query: str, item: vibefpmgui.project_tree.ProjectFile) -> int | None:
47def score(query: str, item: ProjectFile) -> int | None:
48    """How well a file matches what's typed (None: not at all)."""
49    typed = "".join(query.lower().split())
50    name, path = PurePosixPath(item.relative).name.lower(), item.relative.lower()
51    if name.startswith(typed):
52        return 1000
53    if typed in name:
54        return 800
55    if _in_order(typed, name):
56        return 600
57    if typed in path:
58        return 400
59    if _in_order(typed, path):
60        return 200
61    return None

How well a file matches what's typed (None: not at all).

def ranked( query: str, files: list[vibefpmgui.project_tree.ProjectFile], open_paths: list[str] | None = None) -> list[vibefpmgui.project_tree.ProjectFile]:
64def ranked(query: str, files: list[ProjectFile], open_paths: list[str] | None = None) -> list[ProjectFile]:
65    """The files matching ``query``, best first (see the module's description)."""
66    open_order = {path: n for n, path in enumerate(open_paths or [])}
67    if not query.strip():
68        return sorted(files, key=lambda f: (open_order.get(f.path, len(open_order)), f.dependency, f.package,
69                                            f.relative.lower()))
70    matched = []
71    for item in files:
72        points = score(query, item)
73        if points is not None:
74            matched.append((-points, item.dependency, open_order.get(item.path, len(open_order)),
75                            len(item.relative), item.relative.lower(), item))
76    matched.sort(key=lambda m: m[:5])
77    return [m[-1] for m in matched]

The files matching query, best first (see the module's description).

class QuickOpen(PySide6.QtWidgets.QFrame):
129class QuickOpen(QFrame):
130    """The palette: a field to type in, the matching files under it (as the
131    Command Palette). ``opened`` is called with the file chosen."""
132
133    WIDTH = 660
134    ROWS = 14
135
136    def __init__(self, window: QWidget, opened, icon_for=None):
137        super().__init__(window, Qt.Popup)
138        self.window_ = window
139        self.opened = opened
140        self.icon_for = icon_for
141        self.files: list[ProjectFile] = []
142        self.open_paths: list[str] = []
143        self.setFrameShape(QFrame.StyledPanel)
144        self.field = QLineEdit()
145        self.field.setPlaceholderText("Type part of a file's name (letters in order: \"dstat\" finds demo_stats.f90)")
146        self.field.setClearButtonEnabled(True)
147        self.field.textChanged.connect(self._filter)
148        self.field.installEventFilter(self)
149        # Its icon, in front of what's typed (the menu item's: see set_icon).
150        self.icon_action = self.field.addAction(QIcon(), QLineEdit.LeadingPosition)
151        self.list = QListWidget()
152        self.list.setItemDelegate(_FileDelegate(self))
153        self.list.setUniformItemSizes(True)
154        self.list.setFocusPolicy(Qt.NoFocus)
155        self.list.itemClicked.connect(lambda item: self.choose(item.data(FILE_ROLE)))
156        self.note = QLabel()  # (whether ignored files are in the list, and how many more there are)
157        self.note.setStyleSheet("color: gray;")
158        layout = QVBoxLayout(self)
159        layout.setContentsMargins(6, 6, 6, 6)
160        layout.setSpacing(6)
161        layout.addWidget(self.field)
162        layout.addWidget(self.list)
163        layout.addWidget(self.note)
164        self.include_ignored = False
165
166    def set_icon(self, icon: QIcon):
167        """The icon in front of the field (the menu item's, in the theme's colors)."""
168        self.icon_action.setIcon(icon)
169
170    def open(self, files: list[ProjectFile], open_paths: list[str], include_ignored: bool):
171        """Show the palette at the top of the window, for these files."""
172        self.files, self.open_paths, self.include_ignored = files, open_paths, include_ignored
173        self.field.clear()
174        self._filter("")
175        width = min(self.WIDTH, max(320, self.window_.width() - 40))
176        row = self.list.sizeHintForRow(0) if self.list.count() else 24
177        self.resize(width, self.field.sizeHint().height() + row * self.ROWS + self.note.sizeHint().height() + 30)
178        top = self.window_.mapToGlobal(self.window_.rect().topLeft())
179        self.move(top.x() + (self.window_.width() - width) // 2, top.y() + 40)
180        self.show()
181        self.raise_()
182        self.field.setFocus()
183
184    def _filter(self, text: str):
185        self.list.clear()
186        found = ranked(text, self.files, self.open_paths)
187        for item in found[:MAX_SHOWN]:
188            row = QListWidgetItem(item.relative)
189            row.setData(FILE_ROLE, item)
190            row.setToolTip(item.path)
191            self.list.addItem(row)
192        if self.list.count():
193            self.list.setCurrentRow(0)
194        more = f"{len(found) - MAX_SHOWN} more: type more of the name. " if len(found) > MAX_SHOWN else ""
195        which = ("Files ignored by git are included" if self.include_ignored
196                 else "Files ignored by git are left out")
197        self.note.setText(f"{more}{which} (Project files → Ignored). {len(self.files)} files.")
198
199    def choose(self, item: ProjectFile):
200        self.close()
201        self.opened(item.path)
202
203    def eventFilter(self, obj, event):
204        if obj is self.field and event.type() == QEvent.KeyPress:
205            key = event.key()
206            steps = {Qt.Key_Up: -1, Qt.Key_Down: 1, Qt.Key_PageUp: -self.ROWS, Qt.Key_PageDown: self.ROWS}
207            if key in steps and self.list.count():
208                self.list.setCurrentRow(max(0, min(self.list.count() - 1, self.list.currentRow() + steps[key])))
209                return True
210            if key in (Qt.Key_Return, Qt.Key_Enter):
211                item = self.list.currentItem()
212                if item is not None:
213                    self.choose(item.data(FILE_ROLE))
214                return True
215            if key == Qt.Key_Escape:
216                self.close()
217                return True
218        return super().eventFilter(obj, event)

The palette: a field to type in, the matching files under it (as the Command Palette). opened is called with the file chosen.

QuickOpen(window: PySide6.QtWidgets.QWidget, opened, icon_for=None)
136    def __init__(self, window: QWidget, opened, icon_for=None):
137        super().__init__(window, Qt.Popup)
138        self.window_ = window
139        self.opened = opened
140        self.icon_for = icon_for
141        self.files: list[ProjectFile] = []
142        self.open_paths: list[str] = []
143        self.setFrameShape(QFrame.StyledPanel)
144        self.field = QLineEdit()
145        self.field.setPlaceholderText("Type part of a file's name (letters in order: \"dstat\" finds demo_stats.f90)")
146        self.field.setClearButtonEnabled(True)
147        self.field.textChanged.connect(self._filter)
148        self.field.installEventFilter(self)
149        # Its icon, in front of what's typed (the menu item's: see set_icon).
150        self.icon_action = self.field.addAction(QIcon(), QLineEdit.LeadingPosition)
151        self.list = QListWidget()
152        self.list.setItemDelegate(_FileDelegate(self))
153        self.list.setUniformItemSizes(True)
154        self.list.setFocusPolicy(Qt.NoFocus)
155        self.list.itemClicked.connect(lambda item: self.choose(item.data(FILE_ROLE)))
156        self.note = QLabel()  # (whether ignored files are in the list, and how many more there are)
157        self.note.setStyleSheet("color: gray;")
158        layout = QVBoxLayout(self)
159        layout.setContentsMargins(6, 6, 6, 6)
160        layout.setSpacing(6)
161        layout.addWidget(self.field)
162        layout.addWidget(self.list)
163        layout.addWidget(self.note)
164        self.include_ignored = False

__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, frameShape: PySide6.QtWidgets.QFrame.Shape | None = None, frameShadow: PySide6.QtWidgets.QFrame.Shadow | None = None, lineWidth: int | None = None, midLineWidth: int | None = None, frameWidth: int | None = None, frameRect: PySide6.QtCore.QRect | None = None) -> None

Initialize self. See help(type(self)) for accurate signature.

WIDTH = 660
ROWS = 14
window_
opened
icon_for
open_paths: list[str]
field
icon_action
list
note
include_ignored
def set_icon(self, icon: PySide6.QtGui.QIcon):
166    def set_icon(self, icon: QIcon):
167        """The icon in front of the field (the menu item's, in the theme's colors)."""
168        self.icon_action.setIcon(icon)

The icon in front of the field (the menu item's, in the theme's colors).

def open( self, files: list[vibefpmgui.project_tree.ProjectFile], open_paths: list[str], include_ignored: bool):
170    def open(self, files: list[ProjectFile], open_paths: list[str], include_ignored: bool):
171        """Show the palette at the top of the window, for these files."""
172        self.files, self.open_paths, self.include_ignored = files, open_paths, include_ignored
173        self.field.clear()
174        self._filter("")
175        width = min(self.WIDTH, max(320, self.window_.width() - 40))
176        row = self.list.sizeHintForRow(0) if self.list.count() else 24
177        self.resize(width, self.field.sizeHint().height() + row * self.ROWS + self.note.sizeHint().height() + 30)
178        top = self.window_.mapToGlobal(self.window_.rect().topLeft())
179        self.move(top.x() + (self.window_.width() - width) // 2, top.y() + 40)
180        self.show()
181        self.raise_()
182        self.field.setFocus()

Show the palette at the top of the window, for these files.

def choose(self, item: vibefpmgui.project_tree.ProjectFile):
199    def choose(self, item: ProjectFile):
200        self.close()
201        self.opened(item.path)
def eventFilter(self, obj, event):
203    def eventFilter(self, obj, event):
204        if obj is self.field and event.type() == QEvent.KeyPress:
205            key = event.key()
206            steps = {Qt.Key_Up: -1, Qt.Key_Down: 1, Qt.Key_PageUp: -self.ROWS, Qt.Key_PageDown: self.ROWS}
207            if key in steps and self.list.count():
208                self.list.setCurrentRow(max(0, min(self.list.count() - 1, self.list.currentRow() + steps[key])))
209                return True
210            if key in (Qt.Key_Return, Qt.Key_Enter):
211                item = self.list.currentItem()
212                if item is not None:
213                    self.choose(item.data(FILE_ROLE))
214                return True
215            if key == Qt.Key_Escape:
216                self.close()
217                return True
218        return super().eventFilter(obj, event)

eventFilter(self, watched: PySide6.QtCore.QObject, event: PySide6.QtCore.QEvent, /) -> bool

staticMetaObject = PySide6.QtCore.QMetaObject("QuickOpen" inherits "QFrame": )