We are going to look at two ways to define scopes for custom language elements - the inherited (hierarchical) and the referential approaches. We chose the Calculator tutorial language as a testbed for our experiments. You can find the calculator-tutorial project included in the set of sample projects that comes with the MPS distribution.
All references need to know the set of allowed targets. This enables MPS to populate the completion menu whenever the user is about to supply a value for the reference. Existing references can be validated against that set and marked as invalid, if they refer to elements out of the scope.
MPS offers two ways to define scopes:
- Inherited scopes
- Reference scopes
Reference scope offers lower ceremony, while Inherited scopes allow the scope to be built gradually following the hierarchy of nodes in the model.
We will describe the new hierarchical (inherited) mechanism of scope resolution first. This mechanism delegates scope resolution to the ancestors, who implement ScopeProvider.
- MPS starts looking for the closest ancestor to the reference node that implements ScopeProvider and who can provide scope for the current kind.
- If the ScopeProvider returns null, we continue searching for more distant ancestors.
- Each ScopeProvider can
- build and return a Scope implementation (more on these later)
- delegate to the parent scope
- add its own elements to the parent scope
- hide elements from parent scope (more on how to work with scopes will be discussed later)
Our InputFieldReference thus searches for InputField nodes and relies on its ancestors to build a list of those.
Once we have specified that the scope for InputFieldReference when searching for an InputField is inherited, we must indicate that Calculator is a ScopeProvider. This ensures that Calculator will have say in building the scope for all InputFieldReferences that are placed as its descendants.
The Calculator in our case should return a list of all its InputFields whenever queried for scope of InputField. So in the Behavior aspect of Calculator we override (Control + O) the getScope() method:
If Scope remains unresolved, we need to import the model (Control + R) that contains it (jetbrains.mps.scope):
We also need BaseLanguage since we need to encode some functionality. The smodel language needs to be imported in order to query nodes. These languages should have been imported for you automatically. If not, you can import them using the Control + L shortcut.
Now we can complete the scope definition code, which, in essence, returns all input fields from within the calculator:
A quick tip: Notice the use of SimpleRoleScope class. It is one of several helper classes that can help you build your own custom scopes. Check them out by Navigating to SimpleRoleScope (Control + N) and opening up the containing package structure (Alt + F1).
A slightly more advanced example can be found in BaseLanguage. VariableReference uses inherited scope for its variableDeclaration reference.
Concepts such as ForStatement, LocalVariableDeclaration, BaseMethodDeclaration, Classifier as well as some others add variable declarations to the scope and thus implement ScopeProvider.
For example, ForStatement uses the Scopes.forVariables helper function to build a scope that enriches the parent scope with all variables declared in the for loop, potentially hiding variables of the same name in the parent scope. The come from expression detects whether the reference that we're currently resolving the scope for lies in the given part of the sub-tree.
Using reference scope
Scopes can alternatively be implemented in a faster but less scalable way - using the reference scope:
Instead of delegating to the ancestors of type ScopeProvider to do the resolution, you can insert the scope resolution code right into the constraint definition.
Instead of the code that originally was inside the Calculator's getScope() method, it is now InputFieldReference itself that defines the scope. The function for reference scope is supposed to return a Scope instance, just like the ScopeProvider.getScope() method. Scope is essentially a list of potential reference targets together with logic to resolve these targets with textual values.
There are several predefined Scope implementations and related helper factory methods ready for you to use:
- SimpleRoleScope - simply adds all nodes connected to the supplied node and being in the specified role
- ModelPlusImportedScope - provides reference targets from imported models. Allows the user add targets to scope by ctrl + R / cmd + R (import containing model).
- FilteringScope - allow you to exclude some elements from another scope. Subclasses of FilteringScope with override the isExcluded() method.
- DelegatingScope - delegates to another scope. Meant to be overridden to customize the behavior of the original scope.
You may also look around yourself in the scope model: