add first proc doc
This commit is contained in:
+60
-27
@@ -44,6 +44,7 @@ from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
|
||||
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||
from tools.inlay_hint import InlayHintGenerator
|
||||
from tools.file_sourcing import get_all_psc_files, read_psc_file
|
||||
from tools.proc_docs import ProcDocCollector
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
|
||||
|
||||
@@ -209,48 +210,70 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
|
||||
else:
|
||||
return None
|
||||
|
||||
# 1) Built-in MOM procs / variables
|
||||
command = token
|
||||
data = standard_items.json_data
|
||||
|
||||
all_items = data.get("MOM_procs", []) + data.get("mom_variables", [])
|
||||
|
||||
match = next((item for item in all_items if item["label"] == command), None)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
if not match.get("kind") == "function":
|
||||
return None
|
||||
|
||||
label = match.get("label", "")
|
||||
|
||||
parameters = match.get("parameters", [])
|
||||
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||
|
||||
example_data = match.get("example", [])
|
||||
example_md = "\n".join(f"{line}" for line in example_data)
|
||||
|
||||
returns_data = match.get("returns", ["None"])
|
||||
returns_md = "\n".join(f"- {line}" for line in returns_data)
|
||||
|
||||
doc_md = f"""\
|
||||
match = next((item for item in all_items if item.get("label") == command), None)
|
||||
if match and match.get("kind") == "function":
|
||||
label = match.get("label", "")
|
||||
parameters = match.get("parameters", [])
|
||||
param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
|
||||
example_data = match.get("example", [])
|
||||
example_md = "\n".join(f"{line}" for line in example_data)
|
||||
returns_data = match.get("returns", ["None"])
|
||||
returns_md = "\n".join(f"- {line}" for line in returns_data)
|
||||
doc_md = f"""\
|
||||
### 📘 {label}
|
||||
|
||||
**Purpose**
|
||||
**Purpose**
|
||||
{match.get("description", "No description available.")}
|
||||
|
||||
**Format**
|
||||
**Format**
|
||||
`{match.get("format", label)}`
|
||||
|
||||
**Parameters**
|
||||
**Parameters**
|
||||
{param_lines}
|
||||
|
||||
**Return value**
|
||||
**Return value**
|
||||
{returns_md}
|
||||
|
||||
**Example**
|
||||
**Example**
|
||||
```tcl
|
||||
{example_md}"""
|
||||
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md))
|
||||
|
||||
# 2) Custom procs collected from the workspace
|
||||
# Find params
|
||||
params_for_proc = None
|
||||
for _file, mapping in LSP_SERVER.proc_signatures.items():
|
||||
if command in mapping:
|
||||
params_for_proc = mapping[command]
|
||||
break
|
||||
|
||||
# Find docs
|
||||
doc_text = None
|
||||
if hasattr(LSP_SERVER, "proc_docs"):
|
||||
for _file, mapping in getattr(LSP_SERVER, "proc_docs", {}).items():
|
||||
if command in mapping:
|
||||
doc_text = mapping[command]
|
||||
break
|
||||
|
||||
if params_for_proc is None and not doc_text:
|
||||
return None
|
||||
|
||||
# Render hover for custom proc
|
||||
param_lines = "\n".join(f"- `{p}`" for p in (params_for_proc or [])) or "_None_"
|
||||
# Keep the doc block as-is; it's already normalized by the collector
|
||||
body = doc_text or ""
|
||||
doc_md = f"""\
|
||||
### 🛠️ {command}
|
||||
|
||||
**Parameters**
|
||||
{param_lines}
|
||||
|
||||
{body}
|
||||
"""
|
||||
return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=doc_md))
|
||||
|
||||
|
||||
@@ -318,6 +341,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
||||
capabilities=lsp.ServerCapabilities(
|
||||
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
|
||||
semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
|
||||
hover_provider=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -337,13 +361,22 @@ def initialized(params: lsp.InitializedParams):
|
||||
if not filepath.exists():
|
||||
continue
|
||||
completion.reset()
|
||||
document = LSP_SERVER.workspace.get_text_document(filepath.as_uri()) # filepath.read_text(encoding="utf-8")
|
||||
document = LSP_SERVER.workspace.get_text_document(filepath.as_uri())
|
||||
tree = LSP_SERVER.parser.parse(document.source)
|
||||
# completions and signatures
|
||||
tree.accept(completion, recurse=True)
|
||||
remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion)
|
||||
LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions
|
||||
remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures)
|
||||
LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures
|
||||
# docs
|
||||
from tools.proc_docs import ProcDocCollector
|
||||
|
||||
doc_collector = ProcDocCollector()
|
||||
tree.accept(doc_collector, recurse=True)
|
||||
if not hasattr(LSP_SERVER, "proc_docs"):
|
||||
LSP_SERVER.proc_docs = {}
|
||||
LSP_SERVER.proc_docs[str(filepath)] = doc_collector.docs
|
||||
except Exception as e:
|
||||
log_to_output(f"Fehler beim Parsen von {filepath}: {e}")
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from plugins.poco_plugin import commands
|
||||
from tools import checks, parser
|
||||
from pygls import server, uris
|
||||
from tools.completion_items import completion, remove_existing_items, remove_shared_keys
|
||||
from tools.proc_docs import ProcDocCollector
|
||||
|
||||
|
||||
DIAGNOSTIC_SOURCE = "nx-post-support"
|
||||
@@ -24,6 +25,7 @@ class TclLanguageServer(server.LanguageServer):
|
||||
self.diagnostics = {}
|
||||
self.poco_completion: dict = {}
|
||||
self.proc_signatures: dict = {}
|
||||
self.proc_docs: dict = {}
|
||||
|
||||
def update_poco_completion_for_file(self, document: TextDocument):
|
||||
"""Update poco_completion for a specific file when it changes"""
|
||||
@@ -34,16 +36,24 @@ class TclLanguageServer(server.LanguageServer):
|
||||
del self.poco_completion[filepath]
|
||||
if filepath in self.proc_signatures:
|
||||
del self.proc_signatures[filepath]
|
||||
if filepath in self.proc_docs:
|
||||
del self.proc_docs[filepath]
|
||||
|
||||
# Parse and extract new completion items
|
||||
# Parse and extract new completion items and docs
|
||||
completion.reset()
|
||||
try:
|
||||
tree = self.parser.parse(document.source)
|
||||
# completions and signatures
|
||||
tree.accept(completion, recurse=True)
|
||||
remove_existing_items(completion.custom_functions, self.poco_completion)
|
||||
self.poco_completion[filepath] = completion.custom_functions
|
||||
remove_shared_keys(self.proc_signatures, completion.proc_signatures)
|
||||
self.proc_signatures[filepath] = completion.proc_signatures
|
||||
|
||||
# proc docs
|
||||
doc_collector = ProcDocCollector()
|
||||
tree.accept(doc_collector, recurse=True)
|
||||
self.proc_docs[filepath] = doc_collector.docs
|
||||
except Exception as e:
|
||||
logging.debug(f"Error parsing {filepath}: {e}")
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from tclint.syntax_tree import Visitor, Command, BareWord
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class ProcDocCollector(Visitor):
|
||||
"""
|
||||
Collects documentation blocks preceding `proc` declarations.
|
||||
|
||||
Heuristic:
|
||||
- Accumulate consecutive comment lines into a pending block.
|
||||
- When a `proc <name> { ... }` command is visited, if there is a pending
|
||||
block whose last line ends immediately above the proc line (same line or
|
||||
previous line), attach the block to that proc name.
|
||||
- Clear the pending block whenever we see a non-comment top-level node before
|
||||
a matching `proc` to avoid associating stale docs.
|
||||
|
||||
The collected docs are plain text (comment markers removed). The consumer can
|
||||
render them as Markdown.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.docs: Dict[str, str] = {}
|
||||
self._pending_lines: List[str] = []
|
||||
self._pending_end_line: Optional[int] = None
|
||||
self._last_visited_node_line: Optional[int] = None
|
||||
|
||||
# Comments are leaf nodes; visit order is sequential across the script
|
||||
def visit_comment(self, comment): # type: ignore[override]
|
||||
# comment.value is the text after '#', trimmed of trailing whitespace by parser
|
||||
line_no = comment.pos[0] if comment.pos else None
|
||||
if self._pending_lines and self._pending_end_line is not None:
|
||||
# If the current comment is directly after the previous one, keep the block,
|
||||
# otherwise start a new block.
|
||||
if line_no is not None and self._pending_end_line is not None and line_no <= self._pending_end_line + 1:
|
||||
pass # continue current block
|
||||
else:
|
||||
# gap; start a new block
|
||||
self._pending_lines = []
|
||||
# Append this line to the pending block
|
||||
value = comment.value.lstrip() if isinstance(comment.value, str) else ""
|
||||
self._pending_lines.append(value)
|
||||
self._pending_end_line = comment.end_pos[0] if comment.end_pos else line_no
|
||||
self._last_visited_node_line = line_no
|
||||
|
||||
def visit_command(self, command: Command): # type: ignore[override]
|
||||
# Any command other than `proc` breaks the association with a pending
|
||||
# doc block; we only attach to immediate `proc`s.
|
||||
routine = command.routine
|
||||
proc_line = command.pos[0] if command.pos else None
|
||||
|
||||
if getattr(routine, "contents", None) == "proc":
|
||||
# Extract proc name (first arg)
|
||||
if not command.args:
|
||||
self._clear_pending()
|
||||
return
|
||||
first = command.args[0]
|
||||
name = getattr(first, "value", None) or getattr(first, "contents", None)
|
||||
if not name or not isinstance(first, BareWord):
|
||||
self._clear_pending()
|
||||
return
|
||||
|
||||
# Attach pending block if it's immediately above the proc (allow a single blank line)
|
||||
attach = False
|
||||
if self._pending_lines and self._pending_end_line is not None and proc_line is not None:
|
||||
if self._pending_end_line in {proc_line, proc_line - 1}:
|
||||
attach = True
|
||||
|
||||
if attach:
|
||||
# Normalize text to Markdown: join lines and trim surrounding divider lines
|
||||
lines = self._normalize_lines(self._pending_lines)
|
||||
self.docs[name] = "\n".join(lines).strip()
|
||||
# Whether attached or not, clear pending to avoid reuse
|
||||
self._clear_pending()
|
||||
else:
|
||||
# Non-proc command before a proc clears any pending doc
|
||||
self._clear_pending()
|
||||
|
||||
self._last_visited_node_line = proc_line
|
||||
|
||||
def _clear_pending(self):
|
||||
self._pending_lines = []
|
||||
self._pending_end_line = None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_lines(lines: List[str]) -> List[str]:
|
||||
# Drop long underscore divider lines commonly used
|
||||
def is_divider(s: str) -> bool:
|
||||
s_stripped = s.strip()
|
||||
return len(s_stripped) >= 5 and set(s_stripped) in ({"_"}, {"-"}, {"="})
|
||||
|
||||
filtered = [ln for ln in lines if not is_divider(ln)]
|
||||
# If the block contains pseudo-tags like <Documentation>, keep them; the
|
||||
# hover can render them as-is or the consumer can improve formatting later.
|
||||
return filtered
|
||||
|
||||
Reference in New Issue
Block a user