gtoolkit icon indicating copy to clipboard operation
gtoolkit copied to clipboard

[Lepiter]: Decompose Annotation Completion Monolith

Open seandenigris opened this issue 2 years ago • 5 comments

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 ])

seandenigris avatar Sep 02 '22 19:09 seandenigris

Extending Lepiter with Custom Snippets and Annotations offers the following explanation:

Screen Shot 2022-09-02 at 9 45 01 PM

It shows that one can add completion independently from the core code of Lepiter.

girba avatar Sep 02 '22 19:09 girba

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.

seandenigris avatar Sep 02 '22 19:09 seandenigris

What do you need that is not in the Word Annotation?

girba avatar Sep 02 '22 20:09 girba

IIRC, for example, completion on parser error, like method names that pop up when you have partially matched aClass>>#

seandenigris avatar Sep 02 '22 20:09 seandenigris

+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.

khinsen avatar Sep 03 '22 10:09 khinsen