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
19 changes: 19 additions & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@
}
]
},
"configurationDefaults": {
"editor.tokenColorCustomizations": {
"textMateRules": [
{ "scope": "comment.line.number-sign.ox", "settings": { "foreground": "#6A9955", "fontStyle": "italic" } },
{ "scope": "keyword.control.block.ox", "settings": { "foreground": "#C586C0", "fontStyle": "bold" } },
{ "scope": "constant.numeric.date.ox", "settings": { "foreground": "#4EC9B0", "fontStyle": "bold" } },
{ "scope": "keyword.operator.flag.ox", "settings": { "foreground": "#FF6B6B", "fontStyle": "bold" } },
{ "scope": "entity.name.section.ox", "settings": { "foreground": "#DCDCAA" } },
{ "scope": "entity.name.function.ox", "settings": { "foreground": "#9CDCFE" } },
{ "scope": "support.type.property-name.ox", "settings": { "foreground": "#569CD6" } },
{ "scope": "constant.numeric.weight.ox", "settings": { "foreground": "#B5CEA8" } },
{ "scope": "constant.numeric.reps.ox", "settings": { "foreground": "#CE9178" } },
{ "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" } }
]
}
},
"dependencies": {
"vscode-languageclient": "^9.0.1"
}
Expand Down
25 changes: 22 additions & 3 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": "#singleline-entry" },
{ "include": "#weigh-in-entry" },
{ "include": "#session-header" },
{ "include": "#item-line" },
{ "include": "#metadata-line" }
Expand All @@ -18,6 +20,23 @@
"match": "^(@session|@exercise|@template|@end)\\b",
"name": "keyword.control.block.ox"
},
"singleline-entry": {
"match": "^(\\d{4}-\\d{2}-\\d{2})\\s+([*!])\\s+([^#@:\\s][^:]*):\\s*(.*)$",
"captures": {
"1": { "name": "constant.numeric.date.ox" },
"2": { "name": "keyword.operator.flag.ox" },
"3": { "name": "entity.name.function.ox" },
"4": { "patterns": [{ "include": "#details" }] }
}
},
"weigh-in-entry": {
"match": "^(\\d{4}-\\d{2}-\\d{2})\\s+(W)\\s+(.*)$",
"captures": {
"1": { "name": "constant.numeric.date.ox" },
"2": { "name": "keyword.operator.flag.ox" },
"3": { "patterns": [{ "include": "#details" }] }
}
},
"session-header": {
"match": "^(\\d{4}-\\d{2}-\\d{2})\\s+([*!W])\\s+(.+)$",
"captures": {
Expand Down Expand Up @@ -54,19 +73,19 @@
"name": "string.quoted.double.ox"
},
"weight": {
"match": "\\b(BW|\\d+(kg|lbs)([+/]\\d+(kg|lbs))*)\\b",
"match": "\\b(BW|\\d+(\\.\\d+)?(g|gram|kg|kilogram|lb|pound|oz|ounce|stone|t|tonne|grain|gr|ct|carat)([+/]\\d+(\\.\\d+)?(g|gram|kg|kilogram|lb|pound|oz|ounce|stone|t|tonne|grain|gr|ct|carat))*)\\b",
"name": "constant.numeric.weight.ox"
},
"rep-scheme": {
"match": "\\b\\d+x\\d+\\b|\\b\\d+(/\\d+)+\\b",
"name": "constant.numeric.reps.ox"
},
"time": {
"match": "\\b\\d+(sec|min|hr)\\b",
"match": "PT(\\d+H(\\d+M(\\d+(\\.\\d+)?S)?)?|\\d+M(\\d+(\\.\\d+)?S)?|\\d+(\\.\\d+)?S)",
"name": "constant.numeric.time.ox"
},
"distance": {
"match": "\\b\\d+(km|mi|m|ft|in)\\b",
"match": "\\b\\d+(\\.\\d+)?(m|meter|metre|km|kilometer|cm|centimeter|mm|millimeter|in|inch|ft|foot|yd|yard|mi|mile|nmi)\\b",
"name": "constant.numeric.distance.ox"
}
}
Expand Down
6 changes: 3 additions & 3 deletions editors/vscode/themes/ox-dark.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,21 @@
"name": "Time",
"scope": "constant.numeric.time.ox",
"settings": {
"foreground": "#D7BA7D"
"foreground": "#DCDCAA"
}
},
{
"name": "Distance",
"scope": "constant.numeric.distance.ox",
"settings": {
"foreground": "#D7BA7D"
"foreground": "#D4A5FF"
}
},
{
"name": "Quoted strings",
"scope": "string.quoted.double.ox",
"settings": {
"foreground": "#CE9178",
"foreground": "#E8B87A",
"fontStyle": "italic"
}
},
Expand Down
2 changes: 2 additions & 0 deletions settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"}
84 changes: 84 additions & 0 deletions src/ox/lsp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Language Server Protocol implementation for ox."""

import re

from lsprotocol import types as lsp
from pygls.lsp.server import LanguageServer
from tree_sitter import Language, Parser
Expand Down Expand Up @@ -63,6 +65,88 @@ def did_save(params: lsp.DidSaveTextDocumentParams):
publish_diagnostics(params.text_document.uri, diagnostics)


def _collect_movement_names(tree) -> set[str]:
"""Walk the tree and collect all movement names from item fields."""
names: set[str] = set()
for node in tree.root_node.children:
if node.type == "singleline_entry":
item = node.child_by_field_name("item")
if item:
names.add(item.text.decode("utf-8"))
elif node.type in ("session_block", "template_block"):
for child in node.children:
if child.type == "item_line":
item = child.child_by_field_name("item")
if item:
names.add(item.text.decode("utf-8"))
return names


_SINGLELINE_PREFIX = re.compile(r"^\d{4}-\d{2}-\d{2}\s+[*!]\s+")


def _cursor_wants_movement(text: str, line: int, col: int, tree) -> bool:
"""Check whether the cursor position is a movement-name context."""
lines = text.split("\n")
if line >= len(lines):
return False
current_line = lines[line]

# Context A: singleline entry — line matches date+flag prefix, cursor after it
m = _SINGLELINE_PREFIX.match(current_line)
if m and col >= m.end():
# Make sure we're not inside a session/template block
node = tree.root_node.descendant_for_point_range((line, col), (line, col))
while node:
if node.type in ("session_block", "template_block"):
return False
node = node.parent
return True

# Context B: inside a session/template block on an item line
stripped = current_line.lstrip()
if stripped.startswith("@") or stripped.startswith("note:"):
return False
node = tree.root_node.descendant_for_point_range((line, col), (line, col))
while node:
if node.type in ("session_block", "template_block"):
# Exclude the header line (date/flag/name line)
header_line = node.start_point[0] + 1 # header is 1 line after @session
if line == header_line:
return False
return True
node = node.parent
return False


@server.feature(
lsp.TEXT_DOCUMENT_COMPLETION,
lsp.CompletionOptions(trigger_characters=[" "]),
)
def completion(params: lsp.CompletionParams) -> lsp.CompletionList:
"""Provide movement name completions."""
document = server.workspace.get_text_document(params.text_document.uri)
text = document.source
tree = _parser.parse(bytes(text, encoding="utf-8"))
line = params.position.line
col = params.position.character

if not _cursor_wants_movement(text, line, col, tree):
return lsp.CompletionList(is_incomplete=False, items=[])

names = _collect_movement_names(tree)
items = [
lsp.CompletionItem(
label=name,
insert_text=name + ": ",
kind=lsp.CompletionItemKind.Value,
detail="movement",
)
for name in sorted(names)
]
return lsp.CompletionList(is_incomplete=False, items=items)


def main():
"""Run the language server."""
server.start_io()
Expand Down
42 changes: 21 additions & 21 deletions tree-sitter-ox/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,48 +29,48 @@ module.exports = grammar({

// Single-line entry: date flag item: details
singleline_entry: ($) =>
seq(
prec.right(seq(
field("date", $.date),
field("flag", $.flag),
field("item", $.item),
":",
optional(field("details", $.details)),
"\n"
),
optional("\n")
)),

// Standalone note entry: date note "text"
note_entry: ($) =>
seq(
prec.right(seq(
field("date", $.date),
"note",
field("text", $.quoted_string),
"\n"
),
optional("\n")
)),

// Weigh-in entry: date W weight optional(T06:30) optional("scale name")
weigh_in_entry: ($) =>
seq(
prec.right(seq(
field("date", $.date),
"W",
field("weight", $.weight),
optional(field("time_of_day", $.time_of_day)),
optional(field("scale", $.quoted_string)),
"\n"
),
optional("\n")
)),

// Query entry: date query "name" "SQL"
query_entry: ($) =>
seq(
prec.right(seq(
field("date", $.date),
"query",
field("name", $.quoted_string),
field("sql", $.quoted_string),
"\n"
),
optional("\n")
)),

// @session block
session_block: ($) =>
seq(
prec.right(seq(
"@session",
"\n",
field("date", $.date),
Expand All @@ -79,30 +79,30 @@ module.exports = grammar({
"\n",
repeat(choice($.item_line, $.note_line)),
"@end",
"\n"
),
optional("\n")
)),

// @exercise block
exercise_block: ($) =>
seq(
prec.right(seq(
"@exercise",
field("name", $.identifier),
"\n",
repeat($.metadata_line),
"@end",
"\n"
),
optional("\n")
)),

// @template block
template_block: ($) =>
seq(
prec.right(seq(
"@template",
field("name", $.name),
"\n",
repeat(choice($.item_line, $.note_line)),
"@end",
"\n"
),
optional("\n")
)),

// Item line within a block: item: details
item_line: ($) =>
Expand Down
Loading
Loading