gtoolkit
gtoolkit copied to clipboard
[Lepiter]: Decompose Annotation Completion Monolith
During Hitchhiker #17: Using Glamorous Toolkit for Scientific Notation - Konrad Hinsen & Tudor Girba, the subject of custom annotation completion came up.
This is an exciting horizon that will open up many possibilities.
However, while this is possible in a POC sense, IMHO it is currently not practical for the average user due to all the built-in annotations being handled in one giant parser. LeWordAnnotation
was a good start, but one needs multiple examples that vary in different ways in order to pick out what one needs for their current use case.
In my own case of OlObjectAnnotationCompletionVisitor references in the video linked above, I flopped around for hours tweaking copy/pasted code without really understanding it, and eventually gave up as "good enough", although there are several shortcomings that I can't figure out how to fix.
Some examples of the current suboptimal state...
How is anyone reasonably supposed to make sense of the following method, which is critical to understanding how annotation completion works:
LeAnnotationCompletionVisitor>>#visitSmaCCError: aSmaCCError
| index token annotationType class className isMeta |
index := (1 to: aSmaCCError dismissedTokens size)
detect: [ :i | (aSmaCCError dismissedTokens at: i) stopPosition = self position ]
ifNone: [ 0 ].
annotationType := aSmaCCError parent name source.
index > 0
ifTrue:
[ token := (aSmaCCError dismissedTokens at: index) value.
token = '|'
ifTrue:
[ ^ self
optionsFor: annotationType
startingWith: ''
ignoring: (self previousOptionsFor: aSmaCCError) ].
token = '='
ifTrue:
[ ^ index > 1
ifTrue: [ self valueForOption: (aSmaCCError dismissedTokens at: index - 1) value startingWith: '' ]
ifFalse: [ self valueForOption: aSmaCCError stackContents last items last name value startingWith: '' ] ].
index > 1
ifTrue:
[ (aSmaCCError dismissedTokens at: index - 1) value = '|'
ifTrue:
[ ^ self
optionsFor: annotationType
startingWith: token
ignoring: (self previousOptionsFor: aSmaCCError) ].
(aSmaCCError dismissedTokens at: index - 1) value = '='
ifTrue:
[ ^ index > 2
ifTrue: [ self valueForOption: (aSmaCCError dismissedTokens at: index - 2) value startingWith: token ]
ifFalse: [ self valueForOption: aSmaCCError stackContents last items last name value startingWith: token ] ] ].
(('class' beginsWith: token)
and: [ aSmaCCError stackContents notEmpty and: [ aSmaCCError stackContents last isKindOf: LeClassAnnotationNode ] ])
ifTrue:
[ ^ self composite
addStream:
{GtInsertTextCompletionAction
labeled: (self strategy labelFor: 'class' withSearch: token)
completion: ('class' allButFirst: token size)
position: self position} asAsyncStream ] ].
index = 0
ifTrue:
[ aSmaCCError stackContents isEmpty
ifTrue:
[ ^ aSmaCCError parent colon stopPosition = self position
ifTrue:
[ self
optionsFor: annotationType
startingWith: ''
ignoring: #() ]
ifFalse:
[ aSmaCCError errorToken stopPosition = self position
ifTrue:
[ self
optionsFor: annotationType
startingWith: aSmaCCError errorToken value
ignoring: #() ] ] ].
(aSmaCCError stackContents last isKindOf: SmaCCToken)
ifTrue:
[ aSmaCCError stackContents last stopPosition = self position
ifTrue:
[ token := aSmaCCError stackContents last value.
token = '>>'
ifTrue:
[ className := (aSmaCCError stackContents at: aSmaCCError stackContents size - 1) value.
isMeta := className = 'class'.
isMeta ifTrue: [ className := (aSmaCCError stackContents at: aSmaCCError stackContents size - 2) value ].
Smalltalk at: className asSymbol ifPresent: [ :cls | class := isMeta ifTrue: [ cls class ] ifFalse: [ cls ] ].
^ self
selectorCompletionsFor: class
startingWith: ''
isExample: aSmaCCError parent name source = 'gtExample' ].
(token = '=' and: [ (aSmaCCError stackContents at: aSmaCCError stackContents size - 1) value = 'name' ])
ifTrue: [ ^ self classesStartingWith: '' ].
token first isUppercase ifTrue: [ ^ self classesStartingWith: token ] ] ] ].
^ self visitSmaCCParseNode: aSmaCCError
And the slightly less horrifying, but obscure:
LeAnnotationCompletionVisitor>>#optionsFor: annotationType startingWith: aString ignoring: aCollection
| values |
values := #().
annotationType = 'gtClass' ifTrue: [ values := #(name label full expanded show height) ].
annotationType = 'gtPackage' ifTrue: [ values := #(name label tag expanded show height) ].
annotationType = 'gtMethod' ifTrue: [ values := #(name label expanded show height) ].
annotationType = 'gtExample'
ifTrue: [ values := #(name label expanded codeExpanded noCode previewHeight previewExpanded previewShow alignment return) ].
annotationType = 'gtTodo' ifTrue: [ values := #(label due completed) ].
self composite
addStream:
((values asAsyncStream
filter: [ :each | (aString isEmpty or: [ each beginsWith: aString ]) and: [ (aCollection includes: each asString) not ] ])
collect:
[ :each |
| name hasOptionalValue |
hasOptionalValue := self optionalValues includes: each.
name := each , (hasOptionalValue ifTrue: [ '' ] ifFalse: [ '=' ]).
(GtInsertTextCompletionAction
labeled: (self strategy labelFor: name withSearch: aString)
completion: (name allButFirst: aString size)
position: self position) partial: hasOptionalValue not ])
Extending Lepiter with Custom Snippets and Annotations
offers the following explanation:

It shows that one can add completion independently from the core code of Lepiter.
Yes, the broad strokes are laid out in the book, but when one actually tries to implement something, it is nearly impossible to use features of built-in annotations (other than Word Annotation) for inspiration because: 1) they are all lumped together and 2) the code needs a serious intention-revealing refactor.
What do you need that is not in the Word Annotation?
IIRC, for example, completion on parser error, like method names that pop up when you have partially matched aClass>>#
+1 for better explanations/examples for dealing with parse errors. When I type/edit my Leibniz annotations, they are almost always in a state that doesn't parse. And yet, it would often be useful to provide completion based on a smaller local context, even if the surrounding code doesn't parse.