diff --git a/src/Gt4beam/ElixirBodySubstringFilter.class.st b/src/Gt4beam/ElixirBodySubstringFilter.class.st new file mode 100644 index 0000000..e45abce --- /dev/null +++ b/src/Gt4beam/ElixirBodySubstringFilter.class.st @@ -0,0 +1,59 @@ +Class { + #name : 'ElixirBodySubstringFilter', + #superclass : 'ElixirFunctionsFilter', + #instVars : [ + 'substring' + ], + #category : 'Gt4beam-Filters', + #package : 'Gt4beam', + #tag : 'Filters' +} + +{ #category : 'instance creation' } +ElixirBodySubstringFilter class >> forSubstring: value [ + ^ self new substring: value +] + +{ #category : 'descriptors' } +ElixirBodySubstringFilter class >> globalFilterDescriptor2 [ + ^ GtFilterTextModel new + creator: [ :value | self forSubstring: value ]; + named: 'Substring'; + order: 10; + yourself +] + +{ #category : 'descriptors' } +ElixirBodySubstringFilter >> filterDescriptor2For: aMethodsCoder [ + ^ (super filterDescriptor2For: aMethodsCoder) + ifNotNil: [ :aFilterModel | + aFilterModel + text: substring; + yourself ] +] + +{ #category : 'accessing' } +ElixirBodySubstringFilter >> filterValueString [ + ^ self substring +] + +{ #category : 'highlighting' } +ElixirBodySubstringFilter >> highlightRangesIn: aString [ + ^ self rangesOf: self substring in: aString +] + +{ #category : 'testing' } +ElixirBodySubstringFilter >> matches: anEntry [ + ^ (anEntry attributeAt: #source) asString asLowercase + includesSubstring: self substring asLowercase +] + +{ #category : 'accessing' } +ElixirBodySubstringFilter >> substring [ + ^ substring +] + +{ #category : 'accessing' } +ElixirBodySubstringFilter >> substring: anObject [ + substring := anObject +] diff --git a/src/Gt4beam/ElixirFunctionCoder.class.st b/src/Gt4beam/ElixirFunctionCoder.class.st index 2cc3c70..4503805 100644 --- a/src/Gt4beam/ElixirFunctionCoder.class.st +++ b/src/Gt4beam/ElixirFunctionCoder.class.st @@ -208,6 +208,15 @@ ElixirFunctionCoder >> hasSource [ ^ startLine isNotNil and: [ startLine > 0 ] ] +{ #category : 'add-ons' } +ElixirFunctionCoder >> highlightFiltersFor: anAst into: coderAddOns [ + "Underline the active substring filters matches in the source. A + pragma (not initializeAddOns:) so it is re-collected + re-run on every addOns rebuild." + + + coderAddOns addStyler: ElixirSubstringHighlightStyler new +] + { #category : 'as yet unclassified' } ElixirFunctionCoder >> initializeAddOns: addOns [ super initializeAddOns: addOns. diff --git a/src/Gt4beam/ElixirFunctionFilterExamples.class.st b/src/Gt4beam/ElixirFunctionFilterExamples.class.st new file mode 100644 index 0000000..790abcb --- /dev/null +++ b/src/Gt4beam/ElixirFunctionFilterExamples.class.st @@ -0,0 +1,68 @@ +" +I demonstrate the Functions-list filters' pure logic: kind +normalisation, substring matching, and the match/highlight agreement. +" +Class { + #name : 'ElixirFunctionFilterExamples', + #superclass : 'Object', + #category : 'Gt4beam-Examples', + #package : 'Gt4beam', + #tag : 'Examples' +} + +{ #category : 'examples' } +ElixirFunctionFilterExamples >> bodyFilterMatchesCaseInsensitively [ + "The Body filter finds a substring anywhere in the source, + regardless of case, like its Name sibling." + + + | filter | + filter := ElixirBodySubstringFilter forSubstring: 'MNESIA'. + self assert: (filter matches: (self entryNamed: 'go' kind: 'def' + source: 'def go, do: :mnesia.transaction(fn -> :ok end)')). + self assert: (filter matches: (self entryNamed: 'go' kind: 'def' + source: 'def go, do: :ets.lookup(:t, :k)')) not. + ^ filter +] + +{ #category : 'support' } +ElixirFunctionFilterExamples >> entryNamed: aName kind: aKind source: aSource [ + ^ BeamFunctionEntry fields: (Dictionary new + at: 'name' put: aName; + at: 'kind' put: aKind; + at: 'source' put: aSource; + yourself) +] + +{ #category : 'examples' } +ElixirFunctionFilterExamples >> kindFilterNormalisesKindShapes [ + "The bridge sends kind as 'def'/'type' or #def/#type depending on + the eval path; entryKind: strips the # so one filter covers both." + + + | filter | + filter := ElixirKindFilter forKind: 'Types'. + self assert: (filter matches: (self entryNamed: 't' kind: 'type' source: '@type t :: atom')). + self assert: (filter matches: (self entryNamed: 't' kind: '#type' source: '@type t :: atom')). + self assert: (filter matches: (self entryNamed: 'go' kind: 'def' source: 'def go, do: :ok')) not. + ^ filter +] + +{ #category : 'examples' } +ElixirFunctionFilterExamples >> nameFilterHighlightAgreesWithMatch [ + "What matches is what lights up: an uppercase query matches a + lowercase name AND highlights it; the ranges index the original + string." + + + | filter source ranges | + filter := ElixirNameSubstringFilter forSubstring: 'GET'. + self assert: (filter matches: (self entryNamed: 'get_user' kind: 'def' + source: 'def get_user, do: :ok')). + source := 'def get_user, do: forget()'. + ranges := filter highlightRangesIn: source. + self assert: ranges size equals: 2. + ranges do: [ :r | + self assert: (source copyFrom: r first to: r last) asLowercase equals: 'get' ]. + ^ filter +] diff --git a/src/Gt4beam/ElixirFunctionFiltersBuilder.class.st b/src/Gt4beam/ElixirFunctionFiltersBuilder.class.st new file mode 100644 index 0000000..9b2329b --- /dev/null +++ b/src/Gt4beam/ElixirFunctionFiltersBuilder.class.st @@ -0,0 +1,31 @@ +Class { + #name : 'ElixirFunctionFiltersBuilder', + #superclass : 'Object', + #instVars : [ + 'coders' + ], + #category : 'Gt4beam-Filters', + #package : 'Gt4beam', + #tag : 'Filters' +} + +{ #category : 'accessing' } +ElixirFunctionFiltersBuilder >> availableFilters [ + "All Elixir function filters' descriptor-2 models, sorted by order (the + menu source)." + | available | + coders ifNil: [ ^ #() ]. + available := SortedCollection sortBlock: [ :a :b | a order < b order ]. + ElixirFunctionsFilter withAllSubclassesDo: [ :each | + each filterDescriptors2For: coders into: available ]. + ^ available +] + +{ #category : 'accessing' } +ElixirFunctionFiltersBuilder >> coders [ + ^ coders +] + +{ #category : 'accessing' } +ElixirFunctionFiltersBuilder >> coders: aCoders [ + coders := aCoders +] diff --git a/src/Gt4beam/ElixirFunctionStreamingModel.class.st b/src/Gt4beam/ElixirFunctionStreamingModel.class.st index db5ff60..5424680 100644 --- a/src/Gt4beam/ElixirFunctionStreamingModel.class.st +++ b/src/Gt4beam/ElixirFunctionStreamingModel.class.st @@ -60,13 +60,10 @@ ElixirFunctionStreamingModel >> newCoderFor: anEntry [ { #category : 'streaming' } ElixirFunctionStreamingModel >> newItemsStream [ - "I return function entries as an async stream in the order - GtBridge.Analysis.all_functions/1 emits them. The BEAM-side - already groups default-arg arity siblings (start: 0) above - their AST counterpart and parks orphan macro-generated entries - at the end, so re-sorting by start would just lump every - start: 0 entry at the top." - ^ moduleCoder functionEntries asAsyncStream + "additionalFilters (not compositeFilter): the null main filter isn't a + predicate, and entries keep all_functions/1 order via select:." + ^ (moduleCoder functionEntries select: [ :e | + self additionalFilters allSatisfy: [ :f | f matches: e ] ]) asAsyncStream ] { #category : 'announcements' } @@ -78,6 +75,12 @@ ElixirFunctionStreamingModel >> onModuleSourceCodeChanged: anAnnouncement [ self refreshStream ] +{ #category : 'streaming' } +ElixirFunctionStreamingModel >> refreshItemsStreamDueTo: aReason [ + super refreshItemsStreamDueTo: aReason. + self restyleHighlightCoders +] + { #category : 'as yet unclassified' } ElixirFunctionStreamingModel >> refreshStream [ "Drops cached entries whose key isn't in the fresh set — @@ -96,3 +99,12 @@ ElixirFunctionStreamingModel >> refreshStream [ (freshKeys includes: k) ifFalse: [ coders removeKey: k ] ] ]. self refreshItemsStreamDueTo: 'functions reordered' ] + +{ #category : 'streaming' } +ElixirFunctionStreamingModel >> restyleHighlightCoders [ + "Rebuild each cached coder's addOns so the highlight styler re-reads + the active substrings and re-underlines (or clears) after a filter change." + + (mutex critical: [ coders values asArray ]) + do: [ :coder | coder requestUpdateAddOns ] +] diff --git a/src/Gt4beam/ElixirFunctionsFilter.class.st b/src/Gt4beam/ElixirFunctionsFilter.class.st new file mode 100644 index 0000000..ea40688 --- /dev/null +++ b/src/Gt4beam/ElixirFunctionsFilter.class.st @@ -0,0 +1,65 @@ +Class { + #name : 'ElixirFunctionsFilter', + #superclass : 'GtSearchFilter', + #category : 'Gt4beam-Filters', + #package : 'Gt4beam', + #tag : 'Filters' +} + +{ #category : 'descriptors' } +ElixirFunctionsFilter class >> filterDescriptors2For: aCoder into: aCollection [ + (self filterDescriptor2For: aCoder) + ifNotNil: [ :aDescriptor | aCollection add: aDescriptor ] +] + +{ #category : 'comparing' } +ElixirFunctionsFilter >> = anObject [ + "Value-aware (stock GtSearchFilter compares class only) so a changed value re-applies." + ^ self class = anObject class and: [ self filterValueString = anObject filterValueString ] +] + +{ #category : 'matching' } +ElixirFunctionsFilter >> entryKind: anEntry [ + "Normalised kind, '#' stripped: the bridge sends it as 'def'/'type' + (ByteString) or #def/#type (BeamAtomObject) depending on the path." + ^ (anEntry attributeAt: #kind) + ifNil: [ '' ] + ifNotNil: [ :k | k asString copyWithout: $# ] +] + +{ #category : 'comparing' } +ElixirFunctionsFilter >> hash [ + ^ self class hash bitXor: self filterValueString hash +] + +{ #category : 'highlighting' } +ElixirFunctionsFilter >> highlightRangesIn: aString [ + "Source ranges I want highlighted. None by default; substring filters override." + ^ #() +] + +{ #category : 'matching' } +ElixirFunctionsFilter >> isType: anEntry [ + ^ #('type' 'opaque' 'typep') includes: (self entryKind: anEntry) +] + +{ #category : 'matching' } +ElixirFunctionsFilter >> matches: anEntry [ + ^ true +] + +{ #category : 'highlighting' } +ElixirFunctionsFilter >> rangesOf: aSubstring in: aString [ + "Case-insensitive, agreeing with the filters' matches:. Scans + lowered copies; the ranges index the original string." + | ranges idx lowered target | + (aSubstring isNil or: [ aSubstring isEmpty ]) ifTrue: [ ^ #() ]. + lowered := aSubstring asLowercase. + target := aString asLowercase. + ranges := OrderedCollection new. + idx := target indexOfSubCollection: lowered startingAt: 1. + [ idx > 0 ] whileTrue: [ + ranges add: (idx to: idx + lowered size - 1). + idx := target indexOfSubCollection: lowered startingAt: idx + 1 ]. + ^ ranges +] diff --git a/src/Gt4beam/ElixirKindFilter.class.st b/src/Gt4beam/ElixirKindFilter.class.st new file mode 100644 index 0000000..3eb8ca2 --- /dev/null +++ b/src/Gt4beam/ElixirKindFilter.class.st @@ -0,0 +1,56 @@ +Class { + #name : 'ElixirKindFilter', + #superclass : 'ElixirFunctionsFilter', + #instVars : [ + 'kind' + ], + #category : 'Gt4beam-Filters', + #package : 'Gt4beam', + #tag : 'Filters' +} + +{ #category : 'instance creation' } +ElixirKindFilter class >> forKind: aString [ + ^ self new kind: aString +] + +{ #category : 'descriptors' } +ElixirKindFilter class >> globalFilterDescriptor2 [ + ^ GtFilterShortListModel new + creator: [ :item | self forKind: item itemValue ]; + items: [ #('All' 'Functions' 'Types') ]; + selectedItem: 'All'; + displayAllItems; + name: 'Kind'; + order: 20; + beDefault; + yourself +] + +{ #category : 'descriptors' } +ElixirKindFilter >> filterDescriptor2For: aCoder [ + ^ (super filterDescriptor2For: aCoder) + ifNotNil: [ :aFilterModel | aFilterModel selectedItem: kind; yourself ] +] + +{ #category : 'accessing' } +ElixirKindFilter >> filterValueString [ + ^ kind +] + +{ #category : 'accessing' } +ElixirKindFilter >> kind [ + ^ kind +] + +{ #category : 'accessing' } +ElixirKindFilter >> kind: aString [ + kind := aString +] + +{ #category : 'matching' } +ElixirKindFilter >> matches: anEntry [ + kind = 'Types' ifTrue: [ ^ self isType: anEntry ]. + kind = 'Functions' ifTrue: [ ^ (self isType: anEntry) not ]. + ^ true +] diff --git a/src/Gt4beam/ElixirModuleCoder.class.st b/src/Gt4beam/ElixirModuleCoder.class.st index 1e24eb7..48bfdd8 100644 --- a/src/Gt4beam/ElixirModuleCoder.class.st +++ b/src/Gt4beam/ElixirModuleCoder.class.st @@ -12,7 +12,8 @@ Class { 'sessionId', 'selfObject', 'saving', - 'sourceCache' + 'sourceCache', + 'functionStreamingModel' ], #category : 'Gt4beam-Coder', #package : 'Gt4beam', @@ -167,6 +168,30 @@ ElixirModuleCoder >> buildExampleMapElement [ ^ mondrian root ] +{ #category : 'private' } +ElixirModuleCoder >> buildFunctionsFilterBar: aStreamingModel [ + "GT filter-v2 bar: GtFilterItemsModel renders our filter models as native dropdowns + (Kind default-shown, others via +); the updater syncs picks into additionalFilters. + Also kept as functionStreamingModel so the source highlighter can read the active substrings." + | aModel builder active defaults bar updater | + self functionStreamingModel: aStreamingModel. + aModel := GtFilterItemsModel new. + builder := ElixirFunctionFiltersBuilder new coders: aStreamingModel. + aModel availableFiltersBuilder: builder. + active := (aStreamingModel additionalFilters + collect: [ :f | f filterDescriptor2For: aStreamingModel ]) reject: [ :x | x isNil ]. + defaults := builder availableFilters + select: [ :m | m isDefaultFilterModel and: [ (active includes: m) not ] ]. + aModel items: (Array streamContents: [ :s | s nextPutAll: active. s nextPutAll: defaults ]). + bar := aModel asFiltersElement. + updater := GtFilterMethodCodersAdditionalFiltersUpdater new + coders: aStreamingModel; + filtersModel: aModel. + bar userData at: #elixirFilterUpdater put: updater. + bar constraintsDo: [ :c | c horizontal matchParent. c vertical fitContent ]. + ^ bar +] + { #category : 'private' } ElixirModuleCoder >> buildFunctionsViewElement [ "I build the Functions tab. If focusLine is set, I set a scroll @@ -174,7 +199,7 @@ ElixirModuleCoder >> buildFunctionsViewElement [ scroll via list scrollToItemSuchThat:offset: when the matching item loads (same pattern as GtPharoStreamingMethodsCoderElement). Each function coder starts folded so the user scans the list." - | container addRow streaming vm streamingElement focusStart | + | container addRow streaming vm streamingElement focusStart filterBar | container := BlElement new layout: BlLinearLayout vertical; constraintsDo: [ :c | @@ -212,7 +237,14 @@ ElixirModuleCoder >> buildFunctionsViewElement [ container addChild: addRow; addChild: streamingElement matchParent. - ^ container + filterBar := self buildFunctionsFilterBar: streaming. + "Wrap rather than sibling addRow: the draft pane inserts by index into container." + ^ BlElement new + layout: BlLinearLayout vertical; + constraintsDo: [ :c | c horizontal matchParent. c vertical matchParent ]; + addChild: filterBar; + addChild: container; + yourself ] { #category : 'private' } @@ -358,6 +390,16 @@ ElixirModuleCoder >> functionEntries [ ifFalse: [ result ] ] +{ #category : 'accessing' } +ElixirModuleCoder >> functionStreamingModel [ + ^ functionStreamingModel +] + +{ #category : 'accessing' } +ElixirModuleCoder >> functionStreamingModel: aModel [ + functionStreamingModel := aModel +] + { #category : 'as yet unclassified' } ElixirModuleCoder >> gtDependenciesViewFor: aView context: aContext [ diff --git a/src/Gt4beam/ElixirNameSubstringFilter.class.st b/src/Gt4beam/ElixirNameSubstringFilter.class.st new file mode 100644 index 0000000..c348ac2 --- /dev/null +++ b/src/Gt4beam/ElixirNameSubstringFilter.class.st @@ -0,0 +1,56 @@ +Class { + #name : 'ElixirNameSubstringFilter', + #superclass : 'ElixirFunctionsFilter', + #instVars : [ + 'substring' + ], + #category : 'Gt4beam-Filters', + #package : 'Gt4beam', + #tag : 'Filters' +} + +{ #category : 'instance creation' } +ElixirNameSubstringFilter class >> forSubstring: aString [ + ^ self new substring: aString +] + +{ #category : 'descriptors' } +ElixirNameSubstringFilter class >> globalFilterDescriptor2 [ + ^ GtFilterTextModel new + creator: [ :value | self forSubstring: value ]; + named: 'Name'; + order: 15; + yourself +] + +{ #category : 'descriptors' } +ElixirNameSubstringFilter >> filterDescriptor2For: aCoder [ + ^ (super filterDescriptor2For: aCoder) + ifNotNil: [ :aFilterModel | aFilterModel text: substring; yourself ] +] + +{ #category : 'accessing' } +ElixirNameSubstringFilter >> filterValueString [ + ^ substring +] + +{ #category : 'highlighting' } +ElixirNameSubstringFilter >> highlightRangesIn: aString [ + ^ self rangesOf: self substring in: aString +] + +{ #category : 'matching' } +ElixirNameSubstringFilter >> matches: anEntry [ + ^ (anEntry attributeAt: #name) asString asLowercase + includesSubstring: substring asLowercase +] + +{ #category : 'accessing' } +ElixirNameSubstringFilter >> substring [ + ^ substring +] + +{ #category : 'accessing' } +ElixirNameSubstringFilter >> substring: aString [ + substring := aString +] diff --git a/src/Gt4beam/ElixirSubstringHighlightStyler.class.st b/src/Gt4beam/ElixirSubstringHighlightStyler.class.st new file mode 100644 index 0000000..f306890 --- /dev/null +++ b/src/Gt4beam/ElixirSubstringHighlightStyler.class.st @@ -0,0 +1,47 @@ +Class { + #name : 'ElixirSubstringHighlightStyler', + #superclass : 'GtCoderAstStyler', + #category : 'Gt4beam-Styler', + #package : 'Gt4beam', + #tag : 'Styler' +} + +{ #category : 'accessing' } +ElixirSubstringHighlightStyler class >> highlightAttribute [ + ^ BlTextDecorationAttribute new + underline; + color: BrGlamorousColors textHighlightColor; + yourself +] + +{ #category : 'accessing' } +ElixirSubstringHighlightStyler >> activeFilters [ + "Empty when the coder's functionStreamingModel is nil (no filter bar, e.g. ALClassCoder)." + + | streaming | + streaming := self attachedCoder ifNotNil: [ :c | c moduleCoder functionStreamingModel ]. + ^ streaming ifNil: [ #( ) ] ifNotNil: [ :s | s additionalFilters ] +] + +{ #category : 'accessing' } +ElixirSubstringHighlightStyler >> attachedCoder [ + ^ self coderViewModel ifNotNil: [ :vm | vm coderModel ] +] + +{ #category : 'styling' } +ElixirSubstringHighlightStyler >> style: aText ast: theAst [ + "Underline the source ranges each active filter asks to highlight." + + | attr source | + text := aText. + source := aText asString. + attr := self class highlightAttribute. + self activeFilters + do: [ :filter | + (filter highlightRangesIn: source) + do: [ :range | + self + attribute: attr + from: range first + to: range last ] ] +]