From d4dfbdd8d9ab796ab28b0094513fb9827d2fe47c Mon Sep 17 00:00:00 2001 From: santerijps Date: Fri, 15 May 2026 13:55:00 +0300 Subject: [PATCH] Add Odin syntax highlighting --- assets/highlighting-tests/markdown.md | 6 + assets/highlighting-tests/odin.odin | 259 ++++++++++++++++++++++++++ crates/lsh/definitions/markdown.lsh | 10 + crates/lsh/definitions/odin.lsh | 66 +++++++ 4 files changed, 341 insertions(+) create mode 100644 assets/highlighting-tests/odin.odin create mode 100644 crates/lsh/definitions/odin.lsh diff --git a/assets/highlighting-tests/markdown.md b/assets/highlighting-tests/markdown.md index 555b182f961..4de2fc4dde0 100644 --- a/assets/highlighting-tests/markdown.md +++ b/assets/highlighting-tests/markdown.md @@ -84,3 +84,9 @@ export function greet(name) { def greet(name: str) -> str: return f"hello {name}" ``` + +```odin +greet :: proc(name: string) -> string { + return fmt.tprintf("hello %s", name) +} +``` diff --git a/assets/highlighting-tests/odin.odin b/assets/highlighting-tests/odin.odin new file mode 100644 index 00000000000..c059d504979 --- /dev/null +++ b/assets/highlighting-tests/odin.odin @@ -0,0 +1,259 @@ +// Single-line comment + +/* + * Multi-line + * comment + */ + +#+feature dynamic-literals +package main + +import "core:fmt" + +// Package-level constants +MY_CONST :: 42 +PI :: 3.14159 +NAME :: "Odin" + +// Struct type +Vector3 :: struct { + x, y, z: f32, +} + +// Union type +Value :: union { + int, + f64, + string, +} + +// Enum type +Direction :: enum { + North, + South, + East, + West, +} + +// Bit set +Flags :: bit_set[Direction] + +// Distinct type +Meters :: distinct f32 + +// Procedures +add :: proc(a, b: int) -> int { + return a + b +} + +variadic :: proc(nums: ..int) -> int { + result := 0 + for n in nums { + result += n + } + return result +} + +get_value :: proc() -> (int, bool) { + return 42, true +} + +@(deprecated = "use new_func instead") +old_func :: proc() { + fmt.println("old") +} + +demo_numbers :: proc() { + // Integer and float literals + _ := 42 + _ := 3.14 + _ := 1_000_000 + _ := 0xFF + _ := 0b1010 + _ := 0o77 + _ := 1.5e-3 + _ := 2i // imaginary + + // Language constants + _ := true + _ := false + _ = nil +} + +demo_strings :: proc() { + // Double-quoted strings with escape sequences + _ := "hello, world" + _ := "escape: \" \n \t \\" + + // Raw strings (backtick — no escape processing) + _ := `C:\Windows\notepad.exe` + _ := `no \n escapes here` + + // Rune / character literals + _ := 'A' + _ := '\n' + _ := '世' +} + +demo_control_flow :: proc() { + // If / else if / else + if true { + fmt.println("yes") + } else if false { + fmt.println("no") + } else { + fmt.println("maybe") + } + + // C-style for loop + for i := 0; i < 10; i += 1 { + if i == 5 { continue } + if i == 8 { break } + } + + // Range-based for loop + for i in 0..<10 { + fmt.println(i) + } + + // Switch statement + x := 42 + switch x { + case 1: + fmt.println("one") + case 2, 3: + fmt.println("two or three") + case: + fmt.println("other") + } + + // Fallthrough + switch 0 { + case 0: + fallthrough + case 1: + fmt.println("zero or one") + } + + // When (compile-time conditional) + when ODIN_OS == .Windows { + fmt.println("Windows") + } else { + fmt.println("other OS") + } + + // Defer + defer fmt.println("deferred") + + // or_else / or_break + val := get_value() or_else 0 + _ = val + + for { + get_value() or_break + } +} + +demo_types :: proc() { + // Struct literal + v := Vector3{x = 1, y = 4, z = 9} + fmt.println(v) + + // Union + val: Value = 137 + if i, ok := val.(int); ok { + fmt.println(i) + } + + // Enum and switch + d := Direction.North + switch d { + case .North: + fmt.println("north") + case .South, .East, .West: + fmt.println("other direction") + } + + // Bit set + flags := Flags{.North, .East} + fmt.println(.North in flags) + + // Distinct type + dist := Meters(100.0) + fmt.println(dist) + + // Cast / transmute / auto_cast + x: int = cast(int)3.14 + y := transmute([4]u8)u32(0xDEAD_BEEF) + z := auto_cast x + _, _ = y, z +} + +demo_collections :: proc() { + // Dynamic array + arr := make([dynamic]int) + defer delete(arr) + append(&arr, 1, 2, 3) + fmt.println(arr) + + // Map + m := make(map[string]int) + defer delete(m) + m["key"] = 99 + fmt.println(m["key"]) + + // Pointer + val := 123 + ptr := &val + ptr^ = 456 + fmt.println(val) +} + +demo_builtins :: proc() { + // typeid / type_of + _ = typeid_of(int) + val := 0 + _ = type_of(val) + + // size_of / align_of / offset_of + _ = size_of(Vector3) + _ = align_of(f64) + _ = offset_of(Vector3, y) + + // context + context.user_index = 1 +} + +demo_directives :: proc() { + // Compile-time assert + #assert(size_of(u8) == 1) + + // SOA layout + v: #soa[4]Vector3 + _ = v + + // Partial switch + #partial switch d := Direction.North; d { + case .North: + fmt.println("north") + } + + // Unroll for + #unroll for elem, idx in [3]int{1, 4, 9} { + fmt.println(elem, idx) + } +} + +main :: proc() { + demo_numbers() + demo_strings() + demo_control_flow() + demo_types() + demo_collections() + demo_builtins() + demo_directives() + + fmt.println(add(1, 2)) + fmt.println(variadic(1, 2, 3, 4, 5)) + old_func() +} diff --git a/crates/lsh/definitions/markdown.lsh b/crates/lsh/definitions/markdown.lsh index 77a005fe925..fca215b8613 100644 --- a/crates/lsh/definitions/markdown.lsh +++ b/crates/lsh/definitions/markdown.lsh @@ -60,6 +60,16 @@ pub fn markdown() { if /.*/ {} } } + } else if /(?i:odin)/ { + loop { + await input; + if /\s*```/ { + return; + } else { + odin(); + if /.*/ {} + } + } } else if /(?i:py)/ { loop { await input; diff --git a/crates/lsh/definitions/odin.lsh b/crates/lsh/definitions/odin.lsh new file mode 100644 index 00000000000..fc0529df767 --- /dev/null +++ b/crates/lsh/definitions/odin.lsh @@ -0,0 +1,66 @@ +#[display_name = "Odin"] +#[path = "**/*.odin"] +pub fn odin() { + until /$/ { + yield other; + + if /\/\/.*/ { + yield comment; + } else if /\/\*/ { + loop { + yield comment; + if /\*\// { yield comment; break; } + await input; + } + } else if /"/ { + until /$/ { + yield string; + if /\\./ {} + else if /"/ { yield string; break; } + await input; + } + } else if /`/ { + loop { + yield string; + if /`/ { yield string; break; } + await input; + } + } else if /'/ { + until /$/ { + yield string; + if /\\./ {} + else if /'/ { yield string; break; } + await input; + } + } else if /(?:break|case|continue|do|else|fallthrough|for|if|in|not_in|or_break|or_continue|or_else|or_return|return|switch|when|where)\>/ { + yield keyword.control; + } else if /(?:align_of|auto_cast|bit_field|bit_set|cast|context|defer|distinct|dynamic|enum|foreign|import|map|matrix|offset_of|package|proc|size_of|struct|transmute|type_of|typeid|union|using)\>/ { + yield keyword.other; + } else if /(?:any|b8|b16|b32|b64|bool|complex32|complex64|complex128|cstring|f16|f16be|f16le|f32|f32be|f32le|f64|f64be|f64le|i8|i16|i16be|i16le|i32|i32be|i32le|i64|i64be|i64le|i128|i128be|i128le|int|quaternion64|quaternion128|quaternion256|rawptr|rune|string|u8|u16|u16be|u16le|u32|u32be|u32le|u64|u64be|u64le|u128|u128be|u128le|uint|uintptr)\>/ { + yield keyword.other; + } else if /(?:true|false|nil)\>/ { + yield constant.language; + } else if /#\+\w+/ { + yield keyword.other; + if /(?:.+)\>/ { + yield string; + } + } else if /#\w+/ { + yield keyword.other; + } else if /@/ { + yield markup.link; + } else if /(?i:-?(?:0x[\da-fA-F_]+|0b[01_]+|0o[0-7_]+|[\d_]+\.?[\d_]*|\.[\d_]+)(?:e[+-]?[\d_]+)?i?)/ { + if /\w+/ { + // Invalid numeric literal + } else { + yield constant.numeric; + } + } else if /(\w+)\s*\(/ { + yield $1 as method; + } else if /\w+/ { + // Gobble word chars to align the next iteration on a word boundary. + } + + yield other; + } +}