Skip to content
Vadym Diachenko edited this page Jan 25, 2024 · 10 revisions

An [as of yet] unique feature of GMEdit is in being able to provide contextual syntax completion and syntax highlighting for variables and (2.3) methods.

The premise is as following: if GMEdit knows what a type of an expression is, it can offer you contextual auto-completion (e.g. only showing variables from a specific instance) and better error checking (e.g. warning about assigning a string into a numeric variable or using wrong argument types.

Types and fields are determined as following:

  • Local variables
    Local variable types can be indicated using var v:T shorthand syntax.
  • Built-in functions and variables
    GMEdit comes with type definitions (see /resources/app/api/shared/) for majority of GMS2 functions and most GMS2 functions.
  • User scripts and functions
    Argument types can be indicated using @param or shorthand syntax (function(a:T) in GMS≥2.3, #args a:T for older versions).
    Return type can be indicated using @returns or shorthand syntax (function(...)->ReturnType) in GMS≥2.3.
  • Object and struct variables
    GMEdit will automatically pick up variable names declared at top-level (read: not inside {}) within Create events and 2.3 constructors, including inheritance.
    Variable types can be specified using @is; additional variables can be indicated using @hint or @implements. "Implicit types" can be enabled in linter preferences to auto-derive non-ambiguous types.
  • Global variables
    GMEdit will automatically index global.name and globalvar declarations. Types can be similarly indicated using @is.
  • Macros
    GMEdit is able to expand macros and derive types from code within.
  • self
    Type of self is automatically known in objects and constructors and objects but can be set using @self.
    (note: you can summon self-specific completions without self-prefix by typing a period . out-of-context)

Types in GMEdit

GMEdit has the following special built-in types:

  • any: casts to and from any type.
    If type of something is not known, it is assumed to be any.
  • void: cannot be cast from or to.
    Can be used to mark variables that should never be touched or as a function return value (or, well, lack thereof)
  • bool: boolean values (true/false).
    As this is GML, can cast to/from number implicitly.
  • number: includes any numeric values, such as reals and int64s.
  • int: currently an alias for number and provided for convenience.
  • string: string values and literals.
  • undefined: the built-in undefined value is this type.
  • array, array<T>: a generic array or an array of specified type accordingly.
    You can cast array<int> to array and vice versa, but not array<int> to array<string>, for example.
    You can also use T[] in place of array<T> for convenience.
  • ckarray<K, V>, CustomKeyArray<K, V>: an array with a specific key/index type.
    This is good for cases when the key is technically an integer (e.g. a resource ID) but doesn't implicitly cast to it as far as type checking goes, e.g.
    var spriteData:ckarray<sprite, int> = ...;
    var a = spriteData[spr_somme]; // OK
    var b = spriteData[obj_some]; // shows a warning
    var b = spriteData[0]; // also shows a warning
  • ckstruct<K, V>, CustomKeyStruct<K, V>: same as above, but for structs (and struct[$ key] access).
  • T? (formally Null<T>): specified type or undefined.
    T can be cast to T?, but casting T? back to value requires as - e.g. var v:?int; .. i = v as int.
  • ds_map<K, V>, ds_list<T>, ds_grid<T>, etc.: built-in data structure types.
  • specified_map<...field:type, defaultType>: a map with known keys.
    GMEdit uses this for different kinds of async_load, but you can also use this yourself for JSON - e.g. specified_map<i:int, s:string, void> would allow map[?"i"] and map[?"s"] (and type them accordingly) while forbidding any other key access.
  • object: any object/instance can be cast to this type.
    Handy for type constraints!
  • struct: any struct-based value can be cast to this type.
  • asset: any resource (sprites, objects, etc.) can be cast to this type.
  • type<T>: a reference to a type.
    If you have a @hint-based type, e.g.
    globalvar Some; Some = function(a, b) constructor {
        // ...
    }
    /// @hint new Some(a, b)
    /// @hint Some.staticFunc(a, b)
    referencing Some directly will have it typed as type<Some>.
  • A|B or (A|B): either of types.
    So, for example, you can do number|string, or even more types.
    Parentheses are convenient for grouping - e.g. you can use (A|B|C)[] to declare an array that contains values of types A, B, or C.
    You can cast values of any of types to either-type, but casting from either-type to a specific type requires use of as.
    Can also be used either<A, B...>.
  • function<argType1, argType2, ..., returnType>: a function with a matching signature.
    If a function returns nothing, returnType can be void.
    For functions with trailing arguments, use rest<type>.
    Examples:
    var _clamp:function<number, number, number> = clamp;
    var _min:function<rest<number>, number> = min;
    var _show_debug_message:function<any, void> = show_debug_message;
  • [A, B]: a tuple - that is, a fixed-size array with per-index types.
    Since arrays use less memory than structs, they can be beneficial for small sets of data.
    Examples:
    var v2:tuple<number, string> = [1, "hi!"];
    v2[0] = ""; // will warn
    v2[2] = 0; // also will warn
    
    var v3:tuple<x:number, y:number, z:number> = [1, 2, 3]; // with field labels
    Can also be used as tuple<A, B, ...>;
    If the last type is rest<something>, the tuple will accept trailing values.
  • tuple<enum_name> or `enum_name: automatic tuple for enum items.
    This complements type magic. Types for individual enum items can be defined as following:
    enum v_enum_tuple {
    	an_int, /// @is {int}
    	a_string, /// @is {string}
    	sizeof
    }
  • buffer_auto_type: a special type for buffer-related functions.
    If your function has an argument typed as buffer_type, setting a subsequent argument's or return's type to buffer_auto_type will dynamically change it based on what type was passed in - e.g. int for buffer_s32 or string for buffer_string.
    /// @param {buffer} buf
    /// @param {buffer_type} type
    /// @param {buffer_auto_type} value
    function my_buffer_write(buf, type, value) { ... }
    // my_buffer_write(buf, buffer_s32, "hi") would show a warning
    
    /// @param {buffer} buf
    /// @param {buffer_type} type
    /// @returns {buffer_auto_type} value
    function my_buffer_read(buf, type) { ... }
    // x = my_buffer_read(buf, buffer_string) would show a warning
    GMEdit uses this for a lot of built-in buffer_* functions

X as Y

Allows to explicitly cast an expression to a compatible type - such as casting int? to int or picking a specific one of either-types.

For example,

var sn:string|int = ...;
var i:int = is_string(sn) ? real(sn as string) : sn as int;

In saved file, this becomes a /*#as T*/.

cast X

Allows to explicitly cast an expression to any, bypassing type checks in cases where that might be necessary.

For example,

var s:string = argument0;
if (is_real(s)) s = string_format(cast s, 0, 3); // no warning
// ...

In saved file, this becomes a /*#cast*/.

cast X as Y

Allows to explicitly cast an expression to a type (including "incompatible" ones). Generally used in parenthesis, so typing

(cast buffer_read(b, buffer_s32) as obj_entity).

would show you auto-completion for variables from obj_entity and check for errors accordingly.

Examples

Object basics

Suppose you have obj_enemy with the following Create event:

maxhealth = 10; // @is {number}
my_health = maxhealth; // @is {number}
my_target = noone; // @is {obj_entity}
// attack = function(target) {} // 2.3 method

After saving, if you were to type self. anywhere in the object, you would only get the variables defined in Create event and the built-in variables rather than everything that you have in your project.

Similarly, if you were to type obj_enemy. anywhere in the project, you would only get those same variables.

Local types

With above setup, if you were to write

var e:obj_enemy = instance_nearest(x, y, obj_enemy);

After saving, typing e. would show you only the variables from obj_enemy.

Note: if you are confident that you are writing good code, you can enable "Implicit types for local variables" in Preferences or Project Properties to have types auto-derived for variable declarations with initial values (var v = val).

@self

2.2: Suppose you had scr_enemy_ai that would be called by obj_enemy with the following

/// @self {obj_enemy}
self.my_target = instance_nearest(x, y, obj_player);

After saving, typing self. would show you only the variables from obj_enemy.

2.3 version (much the same, but JSDoc tags sit outside the functions now):

/// @self {obj_enemy}
function scr_enemy_ai() {
	self.my_target = instance_nearest(x, y, obj_player);
}

Interfaces

Suppose you had a pair of functions that represent things that you might assign into constructors/objects:

/// @interface {IHorsable}
function scr_init_horsable() {
	neigh = function(magnitude) { throw "not implemented!" }
}

/// @interface
function scr_twovars() {
	oneVar = 1;
	twoVar = 2;
}

(in 2.2, make that two scripts with /// @interface inside the script and replace function(){} by another script)

You would then be able to mark them for inclusion in auto-completion and highlighting by doing the following in Create event of an object:

/// @implements {IHorsable}
scr_twovars(); /// @implements
myVar = "hi!";

Or, for constructors,

/// @implements {IHorsable}
function Some() constructor {
	// ...
	scr_twovars(); /// @implements
	myVar = "hi!";
}

which would then show you myVar, oneVar, twoVar, and neigh(magnitude) when typing self..

Notes and limitations:

  • Like with most GMEdit features, you'll need to save (Ctrl+S) when adding/changing code that specifies types.
  • For compatibility purposes, object references are not object or type<obj_name>, but just obj_name. With almost all instance_ functions taking either an object or an instance, this can only backfire in instance_create[_depth|_layer] (as you would be able to pass in an instance instead of an object).
  • When using @hint A extends B or constructor inheritance on types with parameters (@template), child's first parameters must match up with parent's parameters.

Better workflow:

Syntax extensions:

  • `vals: $v1 $v2` (template strings)
  • #args (pre-2.3 named arguments)
  • ??= (for pre-GM2022 optional arguments)
  • ?? ?. ?[ (pre-GM2022 null-conditional operators)
  • #lambda (pre-2.3 function literals)
  • => (2.3+ function shorthands)
  • #import (namespaces and aliases)
  • v:Type (local variable types)
  • #mfunc (macros with arguments)
  • #gmcr (coroutines)

Customization:

User-created:

Other:

Clone this wiki locally