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)
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
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.
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