Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions editors/vscode/language-configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
],
"folding": {
"markers": {
"start": "^@(session|exercise|template)",
"end": "^@end"
"start": "^\\s*#",
"end": "^\\s*[^#\\s]"
}
}
}
4 changes: 3 additions & 1 deletion editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@
{ "scope": "constant.numeric.time.ox", "settings": { "foreground": "#DCDCAA" } },
{ "scope": "constant.numeric.distance.ox", "settings": { "foreground": "#D4A5FF" } },
{ "scope": "string.quoted.double.ox", "settings": { "foreground": "#E8B87A", "fontStyle": "italic" } },
{ "scope": "string.unquoted.ox", "settings": { "foreground": "#D4D4D4" } }
{ "scope": "string.unquoted.ox", "settings": { "foreground": "#D4D4D4" } },
{ "scope": "keyword.control.include.ox", "settings": { "foreground": "#C586C0", "fontStyle": "bold" } },
{ "scope": "string.unquoted.include-path.ox", "settings": { "foreground": "#D4D4D4" } }
]
}
},
Expand Down
18 changes: 18 additions & 0 deletions editors/vscode/syntaxes/ox.tmLanguage.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"patterns": [
{ "include": "#comment" },
{ "include": "#block-keyword" },
{ "include": "#include-directive" },
{ "include": "#query-entry" },
{ "include": "#singleline-entry" },
{ "include": "#note-entry" },
{ "include": "#weigh-in-entry" },
Expand All @@ -21,6 +23,22 @@
"match": "^(@session|@exercise|@template|@end)\\b",
"name": "keyword.control.block.ox"
},
"include-directive": {
"match": "^(@include)\\s+(\"[^\"]+\")",
"captures": {
"1": { "name": "keyword.control.include.ox" },
"2": { "name": "string.quoted.double.ox" }
}
},
"query-entry": {
"match": "^(\\d{4}-\\d{2}-\\d{2})\\s+(query)\\s+(\"[^\"]*\")\\s+(\"[^\"]*\")$",
"captures": {
"1": { "name": "constant.numeric.date.ox" },
"2": { "name": "keyword.other.query.ox" },
"3": { "name": "string.quoted.double.ox" },
"4": { "name": "string.quoted.double.ox" }
}
},
"singleline-entry": {
"match": "^(\\d{4}-\\d{2}-\\d{2})\\s+([*!])\\s+([^#@:\\s][^:]*):\\s*(.*)$",
"captures": {
Expand Down
16 changes: 16 additions & 0 deletions editors/vscode/themes/ox-dark.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@
"settings": {
"foreground": "#D4D4D4"
}
},
{
"name": "Include keyword",
"scope": "keyword.control.include.ox",
"settings": {
"foreground": "#C586C0",
"fontStyle": "bold"
}
},
{
"name": "Query keyword",
"scope": "keyword.other.query.ox",
"settings": {
"foreground": "#C586C0",
"fontStyle": "bold"
}
}
]
}
107 changes: 91 additions & 16 deletions src/ox/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter

from ox.parse import process_node
from ox.data import Note, StoredQuery, TrainingLog, TrainingSession, WeighIn
from ox.parse import process_include_directive, process_node
from ox.data import Diagnostic, Note, StoredQuery, TrainingLog, TrainingSession, WeighIn
from ox.db import create_db
from ox.lint import collect_diagnostics
from ox.plugins import GENERATOR_PLUGINS, load_plugins
Expand All @@ -24,18 +24,14 @@
DEFAULT_TABLE_BOX = box.SIMPLE


def parse_file(file_path: Path) -> TrainingLog:
"""Parse a training log file and return TrainingLog object.

Args:
file_path: Path to the training log file
def _parse_single_file(
file_path: Path, parser: Parser
) -> tuple[list, list, list, list, list, list[str]]:
"""Parse a single .ox file without resolving includes.

Returns:
TrainingLog object with parsed sessions
Tuple of (sessions, notes, queries, weigh_ins, diagnostics, include_paths)
"""
language = Language(tree_sitter_ox.language())
parser = Parser(language)

with open(file_path, "r") as f:
data = bytes(f.read(), encoding="utf-8")

Expand All @@ -46,7 +42,11 @@ def parse_file(file_path: Path) -> TrainingLog:
log_notes = []
log_queries = []
log_weigh_ins = []
include_paths = []
for child in root_node.children:
if child.type == "include_directive":
include_paths.append(process_include_directive(child))
continue
result = process_node(child)
if isinstance(result, TrainingSession):
entries.append(result)
Expand All @@ -57,13 +57,88 @@ def parse_file(file_path: Path) -> TrainingLog:
elif isinstance(result, WeighIn):
log_weigh_ins.append(result)

diagnostics = collect_diagnostics(tree)
diagnostics = list(collect_diagnostics(tree))
return entries, log_notes, log_queries, log_weigh_ins, diagnostics, include_paths


def _load_recursive(
file_path: Path,
parser: Parser,
visited: set[Path],
) -> tuple[list, list, list, list, list]:
"""Recursively load a file and its includes with cycle detection.

Returns:
Tuple of (sessions, notes, queries, weigh_ins, diagnostics)
"""
abs_path = file_path.resolve()

if abs_path in visited:
diag = Diagnostic(
line=1,
col=0,
end_line=1,
end_col=0,
message=f"Circular include detected: {file_path}",
severity="warning",
)
return [], [], [], [], [diag]

visited.add(abs_path)

if not abs_path.exists():
diag = Diagnostic(
line=1,
col=0,
end_line=1,
end_col=0,
message=f"Included file not found: {file_path}",
severity="warning",
)
return [], [], [], [], [diag]

entries, notes, queries, weigh_ins, diagnostics, include_paths = _parse_single_file(
abs_path, parser
)

for inc_path in include_paths:
resolved = (abs_path.parent / inc_path).resolve()
inc_entries, inc_notes, inc_queries, inc_weigh_ins, inc_diagnostics = (
_load_recursive(Path(resolved), parser, visited)
)
entries.extend(inc_entries)
notes.extend(inc_notes)
queries.extend(inc_queries)
weigh_ins.extend(inc_weigh_ins)
diagnostics.extend(inc_diagnostics)

return entries, notes, queries, weigh_ins, diagnostics


def parse_file(file_path: Path) -> TrainingLog:
"""Parse a training log file and return TrainingLog object.

Resolves @include directives recursively with cycle detection.

Args:
file_path: Path to the training log file

Returns:
TrainingLog object with parsed sessions
"""
language = Language(tree_sitter_ox.language())
parser = Parser(language)

entries, notes, queries, weigh_ins, diagnostics = _load_recursive(
file_path, parser, visited=set()
)

return TrainingLog(
tuple(entries),
tuple(log_notes),
diagnostics,
tuple(log_queries),
tuple(log_weigh_ins),
tuple(notes),
tuple(diagnostics),
tuple(queries),
tuple(weigh_ins),
)


Expand Down
46 changes: 43 additions & 3 deletions src/ox/lsp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Language Server Protocol implementation for ox."""

import re
from pathlib import Path

from lsprotocol import types as lsp
from pygls.lsp.server import LanguageServer
Expand Down Expand Up @@ -34,34 +35,73 @@ def get_diagnostics(text: str) -> list[lsp.Diagnostic]:
]


def _validate_includes(tree, doc_uri: str) -> list[lsp.Diagnostic]:
"""Check include_directive nodes for missing files."""
diagnostics = []
doc_path = Path(doc_uri.replace("file://", ""))
for node in tree.root_node.children:
if node.type == "include_directive":
path_node = node.child_by_field_name("path")
if path_node:
inc_path = path_node.text.decode("utf-8").strip('"')
resolved = (doc_path.parent / inc_path).resolve()
if not resolved.exists():
diagnostics.append(
lsp.Diagnostic(
range=lsp.Range(
start=lsp.Position(
line=node.start_point[0],
character=path_node.start_point[1],
),
end=lsp.Position(
line=node.end_point[0],
character=path_node.end_point[1],
),
),
message=f"Included file not found: {inc_path}",
severity=lsp.DiagnosticSeverity.Warning,
source="ox",
)
)
return diagnostics


def publish_diagnostics(uri: str, diagnostics: list[lsp.Diagnostic]):
"""Publish diagnostics to the client."""
server.text_document_publish_diagnostics(
lsp.PublishDiagnosticsParams(uri=uri, diagnostics=diagnostics)
)


def _get_all_diagnostics(text: str, uri: str) -> list[lsp.Diagnostic]:
"""Get parse diagnostics and include validation diagnostics."""
tree = _parser.parse(bytes(text, encoding="utf-8"))
diagnostics = get_diagnostics(text)
diagnostics.extend(_validate_includes(tree, uri))
return diagnostics


@server.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
def did_open(params: lsp.DidOpenTextDocumentParams):
"""Handle document open - publish initial diagnostics."""
text = params.text_document.text
diagnostics = get_diagnostics(text)
diagnostics = _get_all_diagnostics(text, params.text_document.uri)
publish_diagnostics(params.text_document.uri, diagnostics)


@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
def did_change(params: lsp.DidChangeTextDocumentParams):
"""Handle document change - update diagnostics."""
document = server.workspace.get_text_document(params.text_document.uri)
diagnostics = get_diagnostics(document.source)
diagnostics = _get_all_diagnostics(document.source, params.text_document.uri)
publish_diagnostics(params.text_document.uri, diagnostics)


@server.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
def did_save(params: lsp.DidSaveTextDocumentParams):
"""Handle document save - refresh diagnostics."""
document = server.workspace.get_text_document(params.text_document.uri)
diagnostics = get_diagnostics(document.source)
diagnostics = _get_all_diagnostics(document.source, params.text_document.uri)
publish_diagnostics(params.text_document.uri, diagnostics)


Expand Down
6 changes: 6 additions & 0 deletions src/ox/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ def process_query_entry(node: Node) -> StoredQuery:
return StoredQuery(name=name, sql=sql, date=date)


def process_include_directive(node: Node) -> str:
"""Extract file path from an include_directive node."""
raw = node.child_by_field_name("path").text.decode("utf-8")
return raw.strip('"')


def process_node(node: Node) -> TrainingSession | Note | StoredQuery | None:
"""Process any node type and return appropriate data structure.

Expand Down
Loading
Loading