The general root of your problem is that only the rules from the context currently at the top of the context stack are used.
In your example here, the context that contains - match: [ pushes the preprocessor context onto the top of the stack. Once that happens the only rules that can match are the \. and the ] to leave.
So, given the input [a [b] c], once the first [ is seen, you’re inside preprocessor where there’s no rule that says that a [ should enter a new nested level; instead that character is consumed by the \. rule, the ] is seen to pop back to the prior context, and now the remainder of the input c] is just highlighted by whatever rules exist in the outer context.
Try something like this:
contexts:
main:
- include: enter-preprocessor
enter-preprocessor:
- match: '\['
push: preprocessor
preprocessor:
- meta_scope: storage.type.qpscl
- include: enter-preprocessor
- match: ']'
pop: true
Now from the main level if there’s a [ seen, it pushes into the preprocessor context, which also contains the rule that says that a [ should push into a new instance of preprocessor, allowing the nested parsing you want.
If you check the scopes, you can see that the first [ scopes as source.qpscl storage.type.qpscl whereas the second [ scopes as source.qpscl storage.type.qpscl storage.type.qpscl (i.e. there are two storage scopes now because it’s nesting on itself).
You may be tempted to put that include directly inside of preprocessor and skip the extra context, but that will generate an error. When syntaxes are compiled, the include statements inject the content of the given context directly into the current one as new instances of the same rule. A context that includes itself will trigger an error about infinite recursion since it doesn’t know when to stop.