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

def set_fortls_enabled(self, on: bool):
52    def set_fortls_enabled(self, on: bool):
53        self.settings.setValue("fortls", on)
54        self._update_language_server()
def go_to_symbol(self):
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)