vibefpmgui.symbol_search

Go to Symbol in Project: find a module, type or procedure by name.

Type part of a name; the language server's matches (from the project and its dependencies) are listed as you type. Enter or double-click opens the file at the symbol.

  1"""Go to Symbol in Project: find a module, type or procedure by name.
  2
  3Type part of a name; the language server's matches (from the project and
  4its dependencies) are listed as you type. Enter or double-click opens the
  5file at the symbol.
  6"""
  7
  8from __future__ import annotations
  9
 10from pathlib import Path
 11
 12from qtpy.QtCore import Qt, QTimer, Signal
 13from qtpy.QtWidgets import (
 14    QDialog,
 15    QLabel,
 16    QLineEdit,
 17    QListWidget,
 18    QListWidgetItem,
 19    QVBoxLayout,
 20)
 21
 22from . import lsp as lsp_mod
 23
 24MAX_RESULTS = 200
 25LOCATION_ROLE = Qt.UserRole + 1
 26
 27
 28class SymbolSearchDialog(QDialog):
 29    chosen = Signal(str, int, int)  # file, line (1-based), column (0-based)
 30
 31    def __init__(self, client: lsp_mod.LspClient, project_dir: Path | None, parent=None):
 32        super().__init__(parent)
 33        self.client = client
 34        self.project_dir = project_dir
 35        self.setWindowTitle("Go to Symbol in Project")
 36        self.resize(640, 420)
 37        self.query = QLineEdit()
 38        self.query.setPlaceholderText("Name of a module, type, procedure…")
 39        self.query.setClearButtonEnabled(True)
 40        self.results = QListWidget()
 41        self.results.setUniformItemSizes(True)
 42        self.note = QLabel("Type part of a name.")
 43        self.note.setStyleSheet("color: gray;")
 44        layout = QVBoxLayout(self)
 45        layout.addWidget(self.query)
 46        layout.addWidget(self.results, 1)
 47        layout.addWidget(self.note)
 48
 49        self._timer = QTimer(self)  # search once typing pauses
 50        self._timer.setSingleShot(True)
 51        self._timer.setInterval(150)
 52        self._timer.timeout.connect(self._search)
 53        self.query.textChanged.connect(self._timer.start)
 54        self.query.returnPressed.connect(self._accept_current)
 55        self.results.itemActivated.connect(self._accept_item)
 56        self.query.installEventFilter(self)
 57        self._asked = ""
 58        self._open_first = False  # Enter was pressed before the answer came
 59
 60    def eventFilter(self, obj, event):
 61        # Up/Down in the text field move through the results.
 62        if obj is self.query and event.type() == event.Type.KeyPress and \
 63                event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown):
 64            self.results.keyPressEvent(event)
 65            return True
 66        return super().eventFilter(obj, event)
 67
 68    def _search(self):
 69        text = self.query.text().strip()
 70        self._asked = text
 71        if not text:
 72            self.results.clear()
 73            self.note.setText("Type part of a name.")
 74            return
 75        if not self.client.workspace_symbols(text, lambda result, asked=text: self._show(asked, result)):
 76            self.note.setText("The language server (fortls) isn't running.")
 77
 78    def _show(self, asked: str, result):
 79        if asked != self._asked:
 80            return  # an older search
 81        symbols = [s for s in result or [] if isinstance(s, dict) and s.get("location")]
 82        lower = asked.lower()
 83        # Exact names first, then names starting with the text, then the rest.
 84        symbols.sort(key=lambda s: (s.get("name", "").lower() != lower,
 85                                    not s.get("name", "").lower().startswith(lower),
 86                                    len(s.get("name", "")), s.get("name", "").lower()))
 87        self.results.clear()
 88        for s in symbols[:MAX_RESULTS]:
 89            path = lsp_mod.from_uri(s["location"]["uri"])
 90            line = s["location"]["range"]["start"]["line"] + 1
 91            column = s["location"]["range"]["start"]["character"]
 92            shown = path
 93            if self.project_dir is not None:
 94                try:
 95                    shown = path.relative_to(self.project_dir.resolve())
 96                except ValueError:
 97                    pass
 98            kind = lsp_mod.SYMBOL_KINDS.get(s.get("kind"), "")
 99            container = f"{s['containerName']}::" if s.get("containerName") else ""
100            item = QListWidgetItem(f"{container}{s.get('name', '')}   ({kind})   —   {shown}:{line}")
101            item.setData(LOCATION_ROLE, (str(path), line, column))
102            item.setToolTip(f"{path}:{line}")
103            self.results.addItem(item)
104        if self.results.count():
105            self.results.setCurrentRow(0)
106            if self._open_first:
107                self._accept_item(self.results.item(0))
108                return
109        more = f" (first {MAX_RESULTS} shown)" if len(symbols) > MAX_RESULTS else ""
110        self.note.setText(f"{len(symbols)} match{'es' * (len(symbols) != 1)}{more} — "
111                          "Enter opens the selected one." if symbols else "No matches.")
112
113    def _accept_current(self):
114        if self._timer.isActive() or self.query.text().strip() != self._asked:
115            self._open_first = True  # Enter before the search ran: open the best match
116            self._timer.stop()
117            self._search()
118            return
119        item = self.results.currentItem()
120        if item is not None:
121            self._accept_item(item)
122
123    def _accept_item(self, item: QListWidgetItem):
124        path, line, column = item.data(LOCATION_ROLE)
125        self.accept()
126        self.chosen.emit(path, line, column)
MAX_RESULTS = 200
LOCATION_ROLE = 257
class SymbolSearchDialog(PySide6.QtWidgets.QDialog):
 29class SymbolSearchDialog(QDialog):
 30    chosen = Signal(str, int, int)  # file, line (1-based), column (0-based)
 31
 32    def __init__(self, client: lsp_mod.LspClient, project_dir: Path | None, parent=None):
 33        super().__init__(parent)
 34        self.client = client
 35        self.project_dir = project_dir
 36        self.setWindowTitle("Go to Symbol in Project")
 37        self.resize(640, 420)
 38        self.query = QLineEdit()
 39        self.query.setPlaceholderText("Name of a module, type, procedure…")
 40        self.query.setClearButtonEnabled(True)
 41        self.results = QListWidget()
 42        self.results.setUniformItemSizes(True)
 43        self.note = QLabel("Type part of a name.")
 44        self.note.setStyleSheet("color: gray;")
 45        layout = QVBoxLayout(self)
 46        layout.addWidget(self.query)
 47        layout.addWidget(self.results, 1)
 48        layout.addWidget(self.note)
 49
 50        self._timer = QTimer(self)  # search once typing pauses
 51        self._timer.setSingleShot(True)
 52        self._timer.setInterval(150)
 53        self._timer.timeout.connect(self._search)
 54        self.query.textChanged.connect(self._timer.start)
 55        self.query.returnPressed.connect(self._accept_current)
 56        self.results.itemActivated.connect(self._accept_item)
 57        self.query.installEventFilter(self)
 58        self._asked = ""
 59        self._open_first = False  # Enter was pressed before the answer came
 60
 61    def eventFilter(self, obj, event):
 62        # Up/Down in the text field move through the results.
 63        if obj is self.query and event.type() == event.Type.KeyPress and \
 64                event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown):
 65            self.results.keyPressEvent(event)
 66            return True
 67        return super().eventFilter(obj, event)
 68
 69    def _search(self):
 70        text = self.query.text().strip()
 71        self._asked = text
 72        if not text:
 73            self.results.clear()
 74            self.note.setText("Type part of a name.")
 75            return
 76        if not self.client.workspace_symbols(text, lambda result, asked=text: self._show(asked, result)):
 77            self.note.setText("The language server (fortls) isn't running.")
 78
 79    def _show(self, asked: str, result):
 80        if asked != self._asked:
 81            return  # an older search
 82        symbols = [s for s in result or [] if isinstance(s, dict) and s.get("location")]
 83        lower = asked.lower()
 84        # Exact names first, then names starting with the text, then the rest.
 85        symbols.sort(key=lambda s: (s.get("name", "").lower() != lower,
 86                                    not s.get("name", "").lower().startswith(lower),
 87                                    len(s.get("name", "")), s.get("name", "").lower()))
 88        self.results.clear()
 89        for s in symbols[:MAX_RESULTS]:
 90            path = lsp_mod.from_uri(s["location"]["uri"])
 91            line = s["location"]["range"]["start"]["line"] + 1
 92            column = s["location"]["range"]["start"]["character"]
 93            shown = path
 94            if self.project_dir is not None:
 95                try:
 96                    shown = path.relative_to(self.project_dir.resolve())
 97                except ValueError:
 98                    pass
 99            kind = lsp_mod.SYMBOL_KINDS.get(s.get("kind"), "")
100            container = f"{s['containerName']}::" if s.get("containerName") else ""
101            item = QListWidgetItem(f"{container}{s.get('name', '')}   ({kind})   —   {shown}:{line}")
102            item.setData(LOCATION_ROLE, (str(path), line, column))
103            item.setToolTip(f"{path}:{line}")
104            self.results.addItem(item)
105        if self.results.count():
106            self.results.setCurrentRow(0)
107            if self._open_first:
108                self._accept_item(self.results.item(0))
109                return
110        more = f" (first {MAX_RESULTS} shown)" if len(symbols) > MAX_RESULTS else ""
111        self.note.setText(f"{len(symbols)} match{'es' * (len(symbols) != 1)}{more} — "
112                          "Enter opens the selected one." if symbols else "No matches.")
113
114    def _accept_current(self):
115        if self._timer.isActive() or self.query.text().strip() != self._asked:
116            self._open_first = True  # Enter before the search ran: open the best match
117            self._timer.stop()
118            self._search()
119            return
120        item = self.results.currentItem()
121        if item is not None:
122            self._accept_item(item)
123
124    def _accept_item(self, item: QListWidgetItem):
125        path, line, column = item.data(LOCATION_ROLE)
126        self.accept()
127        self.chosen.emit(path, line, column)

QDialog(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, sizeGripEnabled: bool | None = None, modal: bool | None = None) -> None

SymbolSearchDialog( client: vibefpmgui.lsp.LspClient, project_dir: pathlib.Path | None, parent=None)
32    def __init__(self, client: lsp_mod.LspClient, project_dir: Path | None, parent=None):
33        super().__init__(parent)
34        self.client = client
35        self.project_dir = project_dir
36        self.setWindowTitle("Go to Symbol in Project")
37        self.resize(640, 420)
38        self.query = QLineEdit()
39        self.query.setPlaceholderText("Name of a module, type, procedure…")
40        self.query.setClearButtonEnabled(True)
41        self.results = QListWidget()
42        self.results.setUniformItemSizes(True)
43        self.note = QLabel("Type part of a name.")
44        self.note.setStyleSheet("color: gray;")
45        layout = QVBoxLayout(self)
46        layout.addWidget(self.query)
47        layout.addWidget(self.results, 1)
48        layout.addWidget(self.note)
49
50        self._timer = QTimer(self)  # search once typing pauses
51        self._timer.setSingleShot(True)
52        self._timer.setInterval(150)
53        self._timer.timeout.connect(self._search)
54        self.query.textChanged.connect(self._timer.start)
55        self.query.returnPressed.connect(self._accept_current)
56        self.results.itemActivated.connect(self._accept_item)
57        self.query.installEventFilter(self)
58        self._asked = ""
59        self._open_first = False  # Enter was pressed before the answer came

__init__(self, /, parent: PySide6.QtWidgets.QWidget | None = None, f: PySide6.QtCore.Qt.WindowType = Default(Qt.WindowFlags), *, sizeGripEnabled: bool | None = None, modal: bool | None = None) -> None

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

def chosen(unknown):

The type of the None singleton.

client
project_dir
query
results
note
def eventFilter(self, obj, event):
61    def eventFilter(self, obj, event):
62        # Up/Down in the text field move through the results.
63        if obj is self.query and event.type() == event.Type.KeyPress and \
64                event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown):
65            self.results.keyPressEvent(event)
66            return True
67        return super().eventFilter(obj, event)

eventFilter(self, arg__1: PySide6.QtCore.QObject, arg__2: PySide6.QtCore.QEvent, /) -> bool

staticMetaObject = PySide6.QtCore.QMetaObject("SymbolSearchDialog" inherits "QDialog": Methods: #41 type=Signal, signature=chosen(QString,int,int), parameters=QString, int, int )