vibefpmgui.window.language
The language server (fortls) for the editor: starting it, symbols, references, rename.
1"""The language server (fortls) for the editor: starting it, symbols, references, rename.""" 2 3from __future__ import annotations 4 5from pathlib import Path 6 7from qtpy.QtWidgets import QDialog, QMessageBox 8 9from .. import lsp 10from ..editor import EditorTab 11from ..rename_dialog import RenamePreviewDialog 12from ..source_text import read_source 13from ..symbol_search import SymbolSearchDialog 14from ..widgets import run_dialog 15 16 17class LanguageMixin: 18 """The language server's features (a part of `vibefpmgui.main_window.MainWindow`).""" 19 20 def _update_language_server(self): 21 """Run fortls for the open project (restarting it if the project or 22 its folders changed), and connect the open Fortran tabs to it.""" 23 if not self.fortls_action.isChecked() or lsp.executable() is None or self.manifest_path is None: 24 self._stop_language_server() 25 return 26 root = self.manifest_path.parent.resolve() 27 args = lsp.server_args(self.graph, root) 28 client = self.lsp_client 29 if client.is_running() and client.root == root and client.args == args: 30 return 31 for editor in self.editors(): 32 editor.detach_lsp() 33 client.start(root, args) 34 for editor in self.editors(): 35 editor.attach_lsp(client) 36 self._update_editor_actions() 37 38 def _stop_language_server(self): 39 for editor in self.editors(): 40 editor.detach_lsp() 41 self.lsp_client.stop() 42 self._update_editor_actions() 43 44 def _language_server_stopped(self, why: str): 45 for editor in self.editors(): 46 editor.detach_lsp() 47 self.statusBar().showMessage(f"{why} — hover, Go to Definition and the outline are off " 48 "until the project is reloaded", 10000) 49 self._update_editor_actions() 50 51 def set_fortls_enabled(self, on: bool): 52 self.settings.setValue("fortls", on) 53 self._update_language_server() 54 55 def go_to_symbol(self): 56 if not self.lsp_client.is_running(): 57 return 58 dialog = SymbolSearchDialog(self.lsp_client, self.manifest_path.parent if self.manifest_path else None, 59 self) 60 dialog.chosen.connect(self.navigate_to) 61 run_dialog(dialog) 62 63 def _open_editor_for(self, path: Path) -> EditorTab | None: 64 path = path.resolve() 65 return next((e for e in self.editors() if e.path == path), None) 66 67 def _file_text(self, path: Path) -> str: 68 """A file's current text: the editor's (maybe unsaved) if it's open.""" 69 editor = self._open_editor_for(path) 70 if editor is not None: 71 return editor.editor.toPlainText() 72 return read_source(path).text 73 74 def _show_references(self, name: str, results: list): 75 roots, _texts, project_dir = self._search_sources() 76 self.references_panel.show_results(name, results, roots, project_dir) 77 self.show_panel(self.references_panel) 78 self.statusBar().showMessage(f"{len(results)} reference{'s' * (len(results) != 1)} to {name}", 5000) 79 80 def _preview_rename(self, old: str, new: str, edits: dict): 81 edits = lsp.add_result_variable_edits(edits, old, new, self._file_text) 82 open_files = {e.path for e in self.editors()} 83 dialog = RenamePreviewDialog(old, new, edits, self._file_text, open_files, 84 self.manifest_path.parent if self.manifest_path else None, 85 self._mono_font(), self) 86 if run_dialog(dialog) != QDialog.Accepted: 87 return 88 changed_open, written, failed, places = 0, 0, [], 0 89 for path in dialog.selected(): # (deleted later: still there now) 90 file_edits = edits.get(path, []) 91 editor = self._open_editor_for(path) 92 if editor is not None: 93 editor.apply_text_edits(file_edits) # unsaved; Undo reverts it 94 changed_open += 1 95 else: 96 try: 97 text, encoding, newline, _mixed = read_source(path) 98 text = lsp.apply_edits(text, file_edits) 99 path.write_bytes(text.replace("\n", newline).encode(encoding, errors="replace")) 100 except OSError as exc: 101 failed.append(f"{path}: {exc}") 102 continue 103 self.lsp_client.file_changed_on_disk(path) 104 written += 1 105 places += len(file_edits) 106 if failed: 107 QMessageBox.warning(self, "Rename Symbol", "Some files could not be changed:\n\n" 108 + "\n".join(failed)) 109 parts = [] 110 if written: 111 parts.append(f"{written} file{'s' * (written != 1)} saved") 112 if changed_open: 113 parts.append(f"{changed_open} open file{'s' * (changed_open != 1)} changed (not saved yet)") 114 self.statusBar().showMessage(f"Renamed {old} → {new} in {places} place{'s' * (places != 1)}" 115 + (f": {', '.join(parts)}" if parts else ""), 10000) 116 self.project_tree.refresh() 117 118 def _lsp_diagnostics(self, path: Path, issues: list): 119 """fortls checked a file (opened or saved): show it in its tab.""" 120 self._lsp_diagnostics_by_file[path] = issues 121 editor = self._open_editor_for(path) 122 if editor is not None: 123 editor.show_lsp_diagnostics(issues)
class
LanguageMixin:
18class LanguageMixin: 19 """The language server's features (a part of `vibefpmgui.main_window.MainWindow`).""" 20 21 def _update_language_server(self): 22 """Run fortls for the open project (restarting it if the project or 23 its folders changed), and connect the open Fortran tabs to it.""" 24 if not self.fortls_action.isChecked() or lsp.executable() is None or self.manifest_path is None: 25 self._stop_language_server() 26 return 27 root = self.manifest_path.parent.resolve() 28 args = lsp.server_args(self.graph, root) 29 client = self.lsp_client 30 if client.is_running() and client.root == root and client.args == args: 31 return 32 for editor in self.editors(): 33 editor.detach_lsp() 34 client.start(root, args) 35 for editor in self.editors(): 36 editor.attach_lsp(client) 37 self._update_editor_actions() 38 39 def _stop_language_server(self): 40 for editor in self.editors(): 41 editor.detach_lsp() 42 self.lsp_client.stop() 43 self._update_editor_actions() 44 45 def _language_server_stopped(self, why: str): 46 for editor in self.editors(): 47 editor.detach_lsp() 48 self.statusBar().showMessage(f"{why} — hover, Go to Definition and the outline are off " 49 "until the project is reloaded", 10000) 50 self._update_editor_actions() 51 52 def set_fortls_enabled(self, on: bool): 53 self.settings.setValue("fortls", on) 54 self._update_language_server() 55 56 def go_to_symbol(self): 57 if not self.lsp_client.is_running(): 58 return 59 dialog = SymbolSearchDialog(self.lsp_client, self.manifest_path.parent if self.manifest_path else None, 60 self) 61 dialog.chosen.connect(self.navigate_to) 62 run_dialog(dialog) 63 64 def _open_editor_for(self, path: Path) -> EditorTab | None: 65 path = path.resolve() 66 return next((e for e in self.editors() if e.path == path), None) 67 68 def _file_text(self, path: Path) -> str: 69 """A file's current text: the editor's (maybe unsaved) if it's open.""" 70 editor = self._open_editor_for(path) 71 if editor is not None: 72 return editor.editor.toPlainText() 73 return read_source(path).text 74 75 def _show_references(self, name: str, results: list): 76 roots, _texts, project_dir = self._search_sources() 77 self.references_panel.show_results(name, results, roots, project_dir) 78 self.show_panel(self.references_panel) 79 self.statusBar().showMessage(f"{len(results)} reference{'s' * (len(results) != 1)} to {name}", 5000) 80 81 def _preview_rename(self, old: str, new: str, edits: dict): 82 edits = lsp.add_result_variable_edits(edits, old, new, self._file_text) 83 open_files = {e.path for e in self.editors()} 84 dialog = RenamePreviewDialog(old, new, edits, self._file_text, open_files, 85 self.manifest_path.parent if self.manifest_path else None, 86 self._mono_font(), self) 87 if run_dialog(dialog) != QDialog.Accepted: 88 return 89 changed_open, written, failed, places = 0, 0, [], 0 90 for path in dialog.selected(): # (deleted later: still there now) 91 file_edits = edits.get(path, []) 92 editor = self._open_editor_for(path) 93 if editor is not None: 94 editor.apply_text_edits(file_edits) # unsaved; Undo reverts it 95 changed_open += 1 96 else: 97 try: 98 text, encoding, newline, _mixed = read_source(path) 99 text = lsp.apply_edits(text, file_edits) 100 path.write_bytes(text.replace("\n", newline).encode(encoding, errors="replace")) 101 except OSError as exc: 102 failed.append(f"{path}: {exc}") 103 continue 104 self.lsp_client.file_changed_on_disk(path) 105 written += 1 106 places += len(file_edits) 107 if failed: 108 QMessageBox.warning(self, "Rename Symbol", "Some files could not be changed:\n\n" 109 + "\n".join(failed)) 110 parts = [] 111 if written: 112 parts.append(f"{written} file{'s' * (written != 1)} saved") 113 if changed_open: 114 parts.append(f"{changed_open} open file{'s' * (changed_open != 1)} changed (not saved yet)") 115 self.statusBar().showMessage(f"Renamed {old} → {new} in {places} place{'s' * (places != 1)}" 116 + (f": {', '.join(parts)}" if parts else ""), 10000) 117 self.project_tree.refresh() 118 119 def _lsp_diagnostics(self, path: Path, issues: list): 120 """fortls checked a file (opened or saved): show it in its tab.""" 121 self._lsp_diagnostics_by_file[path] = issues 122 editor = self._open_editor_for(path) 123 if editor is not None: 124 editor.show_lsp_diagnostics(issues)
The language server's features (a part of vibefpmgui.main_window.MainWindow).