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
80 changes: 79 additions & 1 deletion src/cdl_lsp/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,30 @@

# Import from cdl-parser where possible
try:
from cdl_parser import (
CRYSTAL_SYSTEMS as _SYSTEMS,
)
from cdl_parser import (
NAMED_FORMS as _FORMS,
)
from cdl_parser import (
POINT_GROUPS as _GROUPS,
)
from cdl_parser import (
TWIN_LAWS as _TWINS,
)
from cdl_parser import (
FEATURE_NAMES as _FEAT_NAMES,
)
from cdl_parser import (
PHENOMENON_TYPES as _PHEN_TYPES,
)

Check failure on line 33 in src/cdl_lsp/constants.py

View workflow job for this annotation

GitHub Actions / lint

ruff (I001)

src/cdl_lsp/constants.py:16:5: I001 Import block is un-sorted or un-formatted

CRYSTAL_SYSTEMS: set[str] = set(_SYSTEMS)
NAMED_FORMS: dict[str, tuple[int, int, int]] = dict(_FORMS)
TWIN_LAWS: set[str] = set(_TWINS)
FEATURE_NAMES: set[str] = set(_FEAT_NAMES)
PHENOMENON_TYPES: set[str] = set(_PHEN_TYPES)
# Flatten all point groups from all systems
ALL_POINT_GROUPS: set[str] = set()
for _pgs in _GROUPS.values():
Expand Down Expand Up @@ -114,6 +122,24 @@
"gypsum_swallow",
}

FEATURE_NAMES: set[str] = {
# Growth features
"phantom", "sector", "zoning", "skeletal", "dendritic",
# Surface features
"striation", "trigon", "etch_pit", "growth_hillock",
# Inclusion features
"inclusion", "needle", "silk", "fluid", "bubble",
# Color features
"colour", "colour_zone", "pleochroism",
# Other
"lamellar", "banding",
}

PHENOMENON_TYPES: set[str] = {
"asterism", "chatoyancy", "adularescence", "labradorescence",
"play_of_color", "colour_change", "aventurescence", "iridescence",
}

# Default point group for each system
DEFAULT_POINT_GROUPS: dict[str, str] = {
"cubic": "m3m",
Expand All @@ -129,7 +155,7 @@
# Modifications
# =============================================================================

MODIFICATIONS: set[str] = {"elongate", "truncate", "taper", "bevel", "twin"}
MODIFICATIONS: set[str] = {"elongate", "truncate", "taper", "bevel", "twin", "flatten"}

# =============================================================================
# Common Miller indices by system
Expand Down Expand Up @@ -344,6 +370,48 @@
- `twin(spinel)` - Spinel law macle
- `twin(japan)` - Japan V-twin
- `twin(trilling,3)` - Three-part cyclic twin""",
"flatten": """**flatten(axis:ratio)**

Compresses the crystal along the specified axis.

Parameters:
- axis: a, b, or c
- ratio: scaling factor (< 1 flattens)

Example: `flatten(a:0.5)` - compress 50% along a-axis""",
}

FEATURE_DOCS: dict[str, str] = {
"phantom": "**phantom** - Internal growth zones visible as ghost outlines.\n\nValues: count (int), color (str)\n\nExample: `[phantom:3, white]`",
"sector": "**sector** - Sector zoning patterns from differential growth rates.\n\nValues: type (hourglass, hexagonal)\n\nExample: `[sector:hourglass]`",
"zoning": "**zoning** - Color or composition banding.\n\nValues: pattern\n\nExample: `[zoning:concentric]`",
"skeletal": "**skeletal** - Skeletal/hopper growth from rapid crystallization.\n\nValues: ratio (0-1)\n\nExample: `[skeletal:0.4]`",
"dendritic": "**dendritic** - Branching tree-like growth.\n\nValues: density\n\nExample: `[dendritic:fine]`",
"striation": "**striation** - Linear surface markings from growth/twinning.\n\nValues: direction, count\n\nExample: `[striation:parallel, 5]`",
"trigon": "**trigon** - Triangular etch pits on diamond octahedron faces.\n\nValues: density (dense/sparse/moderate)\n\nExample: `[trigon:dense]`",
"etch_pit": "**etch_pit** - Dissolution features on crystal faces.\n\nValues: density\n\nExample: `[etch_pit:sparse]`",
"growth_hillock": "**growth_hillock** - Spiral growth features.\n\nValues: density\n\nExample: `[growth_hillock:moderate]`",
"inclusion": "**inclusion** - Solid mineral inclusions.\n\nValues: mineral name\n\nExample: `[inclusion:rutile]`",
"needle": "**needle** - Needle-like inclusions.\n\nValues: mineral, density (0-1)\n\nExample: `[needle:rutile, 0.3]`",
"silk": "**silk** - Fine needle networks causing optical effects.\n\nValues: pattern (dense/oriented/asterism)\n\nExample: `[silk:dense]`",
"fluid": "**fluid** - Fluid inclusions.\n\nValues: type (two-phase, three-phase)\n\nExample: `[fluid:three-phase]`",
"bubble": "**bubble** - Gas/fluid bubbles (diagnostic for synthetics).\n\nValues: type (spherical, elongated)\n\nExample: `[bubble:spherical]`",
"colour": "**colour** - Crystal color.\n\nValues: color name\n\nExample: `[colour:purple]`",
"colour_zone": "**colour_zone** - Color banding zones.\n\nValues: colors (dash-separated), count\n\nExample: `[colour_zone:pink-green-pink, 3]`",
"pleochroism": "**pleochroism** - Direction-dependent color variation.\n\nValues: colors\n\nExample: `[pleochroism:blue-violet]`",
"lamellar": "**lamellar** - Lamellar structures (e.g., in moonstone).\n\nValues: spacing\n\nExample: `[lamellar:fine]`",
"banding": "**banding** - Visible banding patterns.\n\nValues: type (agate, concentric)\n\nExample: `[banding:agate]`",
}

PHENOMENON_DOCS: dict[str, str] = {
"asterism": "**asterism** - Star effect from oriented needle inclusions.\n\nParams: rays (3,4,6,12), intensity (weak/moderate/strong)\n\nExample: `| phenomenon[asterism:6, intensity:strong]`",
"chatoyancy": "**chatoyancy** - Cat's eye effect from parallel fibers.\n\nParams: sharpness (sharp/diffuse)\n\nExample: `| phenomenon[chatoyancy:sharp]`",
"adularescence": "**adularescence** - Floating blue-white light in moonstone.\n\nParams: intensity, color\n\nExample: `| phenomenon[adularescence:strong, blue]`",
"labradorescence": "**labradorescence** - Spectral color flash in labradorite.\n\nParams: colour\n\nExample: `| phenomenon[labradorescence:blue-green]`",
"play_of_color": "**play_of_color** - Spectral color patches in opal.\n\nParams: intensity (faint/moderate/intense)\n\nExample: `| phenomenon[play_of_color:intense]`",
"colour_change": "**colour_change** - Alexandrite effect (color change with lighting).\n\nParams: colours (dash-separated), intensity\n\nExample: `| phenomenon[colour_change:green-red, strong]`",
"aventurescence": "**aventurescence** - Sparkle effect from metallic inclusions.\n\nParams: colour\n\nExample: `| phenomenon[aventurescence:copper]`",
"iridescence": "**iridescence** - Rainbow colors from thin-film interference.\n\nExample: `| phenomenon[iridescence]`",
}

# =============================================================================
Expand Down Expand Up @@ -431,3 +499,13 @@
def is_valid_modification(name: str) -> bool:
"""Check if a name is a valid modification type."""
return name.lower() in MODIFICATIONS


def is_valid_feature_name(name: str) -> bool:
"""Check if a name is a valid feature name."""
return name.lower() in FEATURE_NAMES


def is_valid_phenomenon_type(name: str) -> bool:
"""Check if a name is a valid phenomenon type."""
return name.lower() in PHENOMENON_TYPES
3 changes: 2 additions & 1 deletion src/cdl_lsp/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from .code_actions import get_code_action_kinds, get_code_actions
from .completion import get_completions
from .definition import get_definition
from .definition import find_document_definitions, get_definition
from .diagnostics import DiagnosticInfo, get_diagnostics, validate_document
from .document_symbols import get_document_symbols
from .explain import explain_cdl, get_explain_result
Expand All @@ -38,6 +38,7 @@
"get_hover_info",
# Definition
"get_definition",
"find_document_definitions",
# Snippets
"get_preset_snippets",
"get_snippet_for_preset",
Expand Down
150 changes: 146 additions & 4 deletions src/cdl_lsp/features/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
COMMON_SCALES,
CRYSTAL_SYSTEMS,
DEFAULT_POINT_GROUPS,
FEATURE_DOCS,
FEATURE_NAMES,
FORM_DOCS,
MODIFICATION_DOCS,
MODIFICATIONS,
NAMED_FORMS,
PHENOMENON_DOCS,
PHENOMENON_TYPES,
POINT_GROUP_DOCS,
POINT_GROUPS,
SYSTEM_DOCS,
Expand All @@ -47,6 +51,10 @@ class CompletionContext(Enum):
MODIFICATION = auto() # Inside modification name
MODIFICATION_PARAM = auto() # Inside modification parameters
TWIN_LAW = auto() # Inside twin() parameters
FEATURE_NAME = auto() # Inside [...] after form, expecting feature name
PHENOMENON_TYPE = auto() # After | phenomenon[, expecting type
REFERENCE = auto() # After $ expecting definition name
DEFINITION_START = auto() # After @ at line start, expecting definition syntax
UNKNOWN = auto()


Expand All @@ -72,17 +80,43 @@ def _detect_context(line: str, col: int) -> tuple[CompletionContext, str]:
if not text_before_stripped:
return (CompletionContext.EMPTY, current_word)

# After $ - reference to a named definition
ref_match = re.search(r"\$(\w*)$", text_before)
if ref_match:
return (CompletionContext.REFERENCE, ref_match.group(1))

# @ at line start - definition syntax
def_match = re.match(r"^\s*@(\w*)$", text_before)
if def_match:
return (CompletionContext.DEFINITION_START, def_match.group(1))

# Inside twin()
twin_match = re.search(r"twin\s*\(\s*(\w*)$", text_before, re.IGNORECASE)
if twin_match:
return (CompletionContext.TWIN_LAW, twin_match.group(1))

# Inside modification parameters
for mod in ["elongate", "truncate", "taper", "bevel"]:
for mod in ["elongate", "truncate", "taper", "bevel", "flatten"]:
mod_match = re.search(rf"{mod}\s*\([^)]*$", text_before, re.IGNORECASE)
if mod_match:
return (CompletionContext.MODIFICATION_PARAM, current_word)

# After | phenomenon[
phenomenon_match = re.search(r"phenomenon\s*\[\s*(\w*)$", text_before, re.IGNORECASE)
if phenomenon_match:
return (CompletionContext.PHENOMENON_TYPE, phenomenon_match.group(1))

# Inside feature brackets [...] after form
# Check if we're after }@scale[ or }[
feature_bracket = re.search(r"\}[^{]*\[[^\]]*$", text_before)
if feature_bracket:
inside = re.search(r"\[([^\]]*)$", text_before)
if inside:
content = inside.group(1)
# If the last item has no colon, we're typing a feature name
if ":" not in content.split(",")[-1]:
return (CompletionContext.FEATURE_NAME, current_word)

# Inside Miller index {}
brace_match = re.search(r"\{[^}]*$", text_before)
if brace_match:
Expand Down Expand Up @@ -174,14 +208,38 @@ def _create_completion_item(
)


def get_completions(line: str, col: int, trigger_character: str | None = None) -> list[Any]:
def _find_definitions_in_text(document_text: str) -> list[tuple[str, str]]:
"""
Find all @name = expression definitions in document text.

Args:
document_text: Full document text

Returns:
List of (name, expression) tuples
"""
definitions = []
for doc_line in document_text.split("\n"):
match = re.match(r"^\s*@(\w+)\s*=\s*(.+)$", doc_line)
if match:
definitions.append((match.group(1), match.group(2).strip()))
return definitions


def get_completions(
line: str,
col: int,
trigger_character: str | None = None,
document_text: str = "",
) -> list[Any]:
"""
Get completion items for the current position.

Args:
line: Current line text
col: Column position (0-based)
trigger_character: The character that triggered completion (if any)
document_text: Full document text (for scanning definitions)

Returns:
List of completion items
Expand Down Expand Up @@ -327,7 +385,7 @@ def get_completions(line: str, col: int, trigger_character: str | None = None) -
)

elif context == CompletionContext.AFTER_PIPE:
# Suggest modifications and twin
# Suggest modifications, twin, and phenomenon
prefix = current_word.lower()

for mod in sorted(MODIFICATIONS):
Expand All @@ -343,6 +401,18 @@ def get_completions(line: str, col: int, trigger_character: str | None = None) -
)
)

# Add phenomenon as an option after pipe
if "phenomenon".startswith(prefix) or not prefix:
items.append(
_create_completion_item(
label="phenomenon",
kind=kind.Function if kind else "Function",
detail="Optical phenomenon",
documentation="Specify an optical phenomenon like asterism, chatoyancy, etc.",
insert_text="phenomenon[",
)
)

elif context == CompletionContext.TWIN_LAW:
# Suggest twin laws
prefix = current_word.lower()
Expand All @@ -360,10 +430,40 @@ def get_completions(line: str, col: int, trigger_character: str | None = None) -
)
)

elif context == CompletionContext.FEATURE_NAME:
prefix = current_word.lower()
for feat in sorted(FEATURE_NAMES):
if feat.startswith(prefix) or not prefix:
doc = FEATURE_DOCS.get(feat, "")
items.append(
_create_completion_item(
label=feat,
kind=kind.Property if kind else "Property",
detail="Feature",
documentation=doc,
insert_text=f"{feat}:",
)
)

elif context == CompletionContext.PHENOMENON_TYPE:
prefix = current_word.lower()
for phen in sorted(PHENOMENON_TYPES):
if phen.startswith(prefix) or not prefix:
doc = PHENOMENON_DOCS.get(phen, "")
items.append(
_create_completion_item(
label=phen,
kind=kind.EnumMember if kind else "EnumMember",
detail="Optical phenomenon",
documentation=doc,
insert_text=f"{phen}:",
)
)

elif context == CompletionContext.MODIFICATION_PARAM:
# Context-sensitive parameter completions
# Detect which modification we're in
for mod in ["elongate", "truncate", "taper", "bevel"]:
for mod in ["elongate", "truncate", "taper", "bevel", "flatten"]:
if re.search(rf"{mod}\s*\(", line, re.IGNORECASE):
if mod == "elongate":
for axis in ["a", "b", "c"]:
Expand All @@ -386,6 +486,48 @@ def get_completions(line: str, col: int, trigger_character: str | None = None) -
detail=f"{{{miller[0]}{miller[1]}{miller[2]}}}",
)
)
elif mod == "flatten":
for axis in ["a", "b", "c"]:
items.append(
_create_completion_item(
label=f"{axis}:",
kind=kind.Property if kind else "Property",
detail="Axis parameter",
documentation=f"Flatten along {axis}-axis",
)
)
break

elif context == CompletionContext.REFERENCE:
# After $, suggest known definition names from the document
prefix = current_word.lower()
definitions = _find_definitions_in_text(document_text)
for def_name, def_expr in definitions:
if def_name.lower().startswith(prefix) or not prefix:
items.append(
_create_completion_item(
label=def_name,
kind=kind.Variable if kind else "Variable",
detail=f"Reference to @{def_name}",
documentation=f"Defined as: `{def_expr}`",
sort_text="0" + def_name,
)
)

elif context == CompletionContext.DEFINITION_START:
# After @ at line start, suggest definition syntax template
items.append(
_create_completion_item(
label="name = expression",
kind=kind.Snippet if kind else "Snippet",
detail="Named definition",
documentation=(
"Define a named CDL expression that can be referenced with `$name`.\n\n"
"Example:\n```\n@prism = {10-10}@1.0\n@body = $prism + {10-11}@0.8\n```"
),
insert_text="name = ",
sort_text="0",
)
)

return items
Loading
Loading