Coverage for vibefpmgui/window/language.py: 80%
94 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""The language server (fortls) for the editor: starting it, symbols, references, rename."""
3from __future__ import annotations
5from pathlib import Path
7from qtpy.QtWidgets import QDialog, QMessageBox
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
17class LanguageMixin:
18 """The language server's features (a part of `vibefpmgui.main_window.MainWindow`)."""
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: 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true
30 return
31 for editor in self.editors(): 31 ↛ 32line 31 didn't jump to line 32 because the loop on line 31 never started
32 editor.detach_lsp()
33 client.start(root, args)
34 for editor in self.editors(): 34 ↛ 35line 34 didn't jump to line 35 because the loop on line 34 never started
35 editor.attach_lsp(client)
36 self._update_editor_actions()
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()
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()
51 def set_fortls_enabled(self, on: bool):
52 self.settings.setValue("fortls", on)
53 self._update_language_server()
55 def go_to_symbol(self):
56 if not self.lsp_client.is_running(): 56 ↛ 58line 56 didn't jump to line 58 because the condition on line 56 was always true
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)
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)
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
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)
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: 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 QMessageBox.warning(self, "Rename Symbol", "Some files could not be changed:\n\n"
108 + "\n".join(failed))
109 parts = []
110 if written: 110 ↛ 112line 110 didn't jump to line 112 because the condition on line 110 was always true
111 parts.append(f"{written} file{'s' * (written != 1)} saved")
112 if changed_open: 112 ↛ 114line 112 didn't jump to line 114 because the condition on line 112 was always true
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()
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)