Better Hovers
Luwu's hovers target luwu-lsp, the language server we maintain alongside the language, and the Luwu VSCode/Code-OSS extension. Every hover on this page was copied unedited from luwu-lsp: point at a dotted underline to see one, or tap it on a touch screen.
What's improved
- Multiline types. Hovers print nested and recursive types over several lines the way you'd write them in code instead of on one long line. Large tables are cut short with their brackets still balanced.
- Related types, with links. Hovering a value also summarizes the classes and types its type refers to and links each one to where it's declared. Verbosity levels decide how much of that is expanded (see More or less detail below).
- Overloads and unions, one per line. An overloaded function prints one signature per line under a comment saying how many overloads it has, and a union of tables lays each member out on its own lines. A method called on a union of classes shows a single signature.
- Doc comments above types. Each type and field with a doc comment shows the first line of that comment right above it in the hover. References in doc comments, like
[Health]or[math.max], become links to their declaration or to the library docs.
Classes at a glance
Hover a class's name to see how to build one and what it holds, private members included. Hover an object of the class and you see only what you can use from outside.
--- A cat that can be fed.
class Cat(public const name: string, private hunger = 3)
--- Feeds the cat, so it's less hungry.
public function feed(self, amount: number = 1): Cat
self.hunger = math.max(self.hunger - amount, 0)
return self
end
end
const nyla = Cat("Nyla")
nyla:feed(2)class Cat(name: string, hunger: number?)
public const name: string
private hunger: number
public function feed(self, amount: number?): Cat
end
A cat that can be fed.
An access specifier that marks this field introduced by the class's primary constructor (field parameter list) as public. This keyword may be omitted when the class has 0 private members.
class Particle private (
public position: Vector2,
public velocity: Vector2,
private mass: number
)
public function spawn(position: Vector2): Particle
return Particle(position, Vector2.zero, 1)
end
end
const p = Particle.spawn(Vector2.zero)
print(p.position) -- all good
print(p.mass) -- runtime error, mass is private
A const modifier goes after the access specifier: public const position: Vector2.
public const name: string
A cat that can be fed.
An access specifier that marks this field introduced by the class's primary constructor (field parameter list) as private. Attempting to access a private field outside the lexical scope of its class raises a runtime error.
class SshKey private (
public const public_key: string,
private const private_key: string
)
public function keygen(): SshKey
const keys = crypt.ssh.keygen()
return SshKey(keys.public, keys.private)
end
end
If any parameter carries an access specifier, every other parameter and every class member must carry one too.
private hunger: number
A cat that can be fed.
public function feed(self: Cat, amount: number?): Cat
Feeds the cat, so it's less hungry.
local amount: number
Feeds the cat, so it's less hungry.
const nyla: Cat
Object of Cat
A cat that can be fed.
class Cat
public const name: string
public function feed(self, amount: number?): Cat
end
class Cat(name: string, hunger: number?)
public const name: string
private hunger: number
public function feed(self, amount: number?): Cat
end
A cat that can be fed.
function Cat:feed(amount: number?): Cat
Method of Cat
Feeds the cat, so it's less hungry.
Access specifiers like public and private explain the rules they bring.
Traits
A class's hover marks the members it gets from a trait, like -- from Health. A trait method returning Self shows the class you called it on: ada is a Player, not just Health.
--- Something that takes damage and can run out of health.
trait Health
expect max_health: number
health = 100
--- Takes `amount` off [Health], never going below zero, like [math.max] would.
function damage(self, amount: number): Self
self.health = math.max(self.health - amount, 0)
return self
end
end
--- A player in the arena.
class Player(public const name: string) implements Health
public max_health = 150
end
const ada = Player("Ada"):damage(30)
print(ada.health)trait Health
expect max_health: number
health: number
function damage<Self>(self, amount: number): Self
end
Something that takes damage and can run out of health.
Marks a member of a trait that every implementing class must declare itself.
trait Tool
expect private model: Model
expect public function equipped?(self) -- `?`: classes may leave this one out
expect function __init(self, props: Props) -- a constructor taking these arguments
end
public function damage<Self>(self: Self, amount: number): Self
Takes amount off Health, never going below zero, like math.max would.
class Player(name: string) implements Health
const name: string
max_health: number
health: number -- from Health
function damage(self, amount: number): Player -- from Health
end
References Health
A player in the arena.
Traits this class implements. To check if an object of a class implements a trait, use class.implements(obj, Trait).
const ada: Player
Object of Player Implements Health
A player in the arena.
class Player implements Health
const name: string
max_health: number
health: number -- from Health
function damage(self, amount: number): Player -- from Health
end
function Player:damage(amount: number): Player
Method of Player
Takes amount off Health, never going below zero, like math.max would.
number
Field of Player
Doc comments link what they mention. [Health] jumps to the trait, and [math.max] links to the library docs.
Keywords explain themselves
Every keyword has a hover, Luwu's keywords included: do expressions, give and when. The same word gets a different hover when it means something else, like if as an expression.
const prices: { [string]: number? } = { apple = 1.25, pear = 0.8 }
const total = do
local sum = 0
for _, fruit in { "apple", "kiwi", "pear" } when const price = prices[fruit] do
sum += price
end
give sum
print(if total > 2 then "pricey" else "cheap")Defines an immutable local binding. It is a syntax error to try to change the value of this binding.
const x = 2
x += 1 -- syntax error
const function foo()
do_something()
end
const total: number
A do expression runs its block and evaluates to the value its give gives.
Create loop statements to iterate over containers (or a set number of times).
-- iterate over an array
const cats: { string } = { "Taz", "Nyla", "Nanuk", "Crazy", "Mr. Purrsalot" }
for index, cat in cats do
print(cat)
end
-- iterate over a map
const animals: { [string]: Animal } = get_animals()
for animal_name, animal in animals do
print(`name: {animal_name}`)
end
-- iterate 10 times
for i = 1, 10 do
print(i)
end
-- iterate through an array backwards
for i = #cats, 1, -1 do
print(i, cat)
end
Separates the clauses of an if condition; the branch runs only when every clause passes.
const price: number?
Exits the enclosing do expression with this value.
Choose between two expressions based on a condition; if expressions evaluate to a value rather than branching control flow.
const name = if typeof(value) == "string" then value else tostring(value)
const level =
if score > 100 then
"Expert"
elseif score > 50 then
"Intermediate"
else
"Beginner"
Unlike if statements, if expressions contain exactly one expression per branch, evaluate to at least one value, require an else branch, and do not terminate with end.
Refinements
A function that narrows a type says so. Hover class.implements to see what it refines, then hover the refined value to see what it became.
trait Named
expect name: string
end
class Cat(name: string) implements Named end
--- The name of anything that has one.
const function name_of(thing: unknown): string?
if class.implements(thing, Named) then
return thing.name
end
return nil
end
const function tag(name: literal<string>): string
return `<{name}>`
end
print(name_of(Cat("Nyla")), tag("main"))function name_of(thing: unknown): string?
The name of anything that has one.
function class.implements(o: unknown, t: trait): boolean
Refines o into an object of t
Returns true if o is an object of a class that implements trait t, whether the class lists t after implements or gets it through another trait that needs it.
If o is not an object, returns false. Raises a runtime error if t is a class rather than a trait (use class.isinstance for classes).
In the type system, this refines o to the trait:
local function describe(value: unknown): string?
if class.implements(value, Named) then
return value.name
end
return nil
end
local thing: Named
This object implements Named
The name of anything that has one.
trait Named
expect name: string
end
string
Field of Named
literal<string>
A literal of string: the value as written, like "-f" or true, rather than any string. Literals exist for strings and booleans.
On a parameter, callers have to pass a literal, and the function body sees string; on a local, the local has the literal's own type. Write literal<S> with a generic S to infer S as the literal (sym("foo") gives Symbol<"foo">).
Passed to a type function (Validate<literal<string>>), the type function checks each argument as written and can reject it with types.error(message). A type alias can name either form for use in parameters.
literal<T> and the other type functions explain themselves too.
More or less detail
In your editor, a hover can show more or less. These commands change it for the rest of the session:
- More and Less (
alt+=andalt+-): expand the types a hover mentions, or keep it short. - Traits (
alt+t): show the traits a class implements in full, below its hover.
The luwu.hover.verbosity setting picks where each session starts.