From 16c8614dfafc6d836ccf1ce8d44aec71d3c5db60 Mon Sep 17 00:00:00 2001 From: Omar AbedelKader <113094232+omarabedelkader@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:40:49 +0100 Subject: [PATCH 1/2] adding the logic for the grouping --- .../CompletionEngine.class.st | 53 +++++----- .../NECMenuMorph.class.st | 41 +++++--- src/NECompletion/NECEntry.class.st | 6 ++ src/NECompletion/NECPrefixGroupEntry.class.st | 96 +++++++++++++++++++ 4 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 src/NECompletion/NECPrefixGroupEntry.class.st diff --git a/src/NECompletion-Morphic/CompletionEngine.class.st b/src/NECompletion-Morphic/CompletionEngine.class.st index 0e5445691dc..f44484d34ca 100644 --- a/src/NECompletion-Morphic/CompletionEngine.class.st +++ b/src/NECompletion-Morphic/CompletionEngine.class.st @@ -144,52 +144,61 @@ CompletionEngine >> handleKeyDownBefore: aKeyboardEvent editor: anEditor [ key := aKeyboardEvent key. (self isMenuOpen not and: [ - self editor atCompletionPosition and: [ - NECPreferences popupShowWithShortcut matches: { aKeyboardEvent } ] ]) - ifTrue: [ + self editor atCompletionPosition and: [ NECPreferences popupShowWithShortcut matches: { aKeyboardEvent } ] ]) ifTrue: [ aKeyboardEvent supressNextKeyPress: true. self openMenu. ^ true ]. - key = KeyboardKey backspace ifTrue: [ - self smartBackspace ifTrue: [ ^ true ] ]. + key = KeyboardKey backspace ifTrue: [ self smartBackspace ifTrue: [ ^ true ] ]. self isMenuOpen ifFalse: [ ^ false ]. ({ KeyboardKey left. + KeyboardKey keypadLeft } includes: key) ifTrue: [ + menuMorph leaveGroup ifTrue: [ ^ true ]. + ^ false ]. + + ({ KeyboardKey right. - KeyboardKey keypadLeft. - KeyboardKey keypadRight } includes: key) ifTrue: [ "just move the caret" - ^ false ]. + KeyboardKey keypadRight } includes: key) ifTrue: [ + menuMorph insertSelected ifTrue: [ ^ true ]. + ^ false ]. ({ KeyboardKey up. KeyboardKey keypadUp } includes: key) ifTrue: [ - menuMorph moveUp. - ^ true ]. + menuMorph moveUp. + ^ true ]. + ({ KeyboardKey down. KeyboardKey keypadDown } includes: key) ifTrue: [ - menuMorph moveDown. - ^ true ]. + menuMorph moveDown. + ^ true ]. + key = KeyboardKey pageUp ifTrue: [ - menuMorph pageUp. - ^ true ]. + menuMorph pageUp. + ^ true ]. + key = KeyboardKey pageDown ifTrue: [ - menuMorph pageDown. - ^ true ]. - ((key = KeyboardKey enter or: [ key = KeyboardKey keypadEnter ]) - and: [ NECPreferences useEnterToAccept ]) ifTrue: [ + menuMorph pageDown. + ^ true ]. + + ((key = KeyboardKey enter or: [ key = KeyboardKey keypadEnter ]) and: [ NECPreferences useEnterToAccept ]) ifTrue: [ menuMorph insertSelected ifTrue: [ ^ true ] ]. + key = KeyboardKey tab ifTrue: [ menuMorph insertSelected ifTrue: [ ^ true ] ]. + key = KeyboardKey backspace ifTrue: [ - editor isCaretBehindChar ifFalse: [ self closeMenu ]. - ^ false ]. + editor isCaretBehindChar ifFalse: [ self closeMenu ]. + ^ false ]. + key = KeyboardKey escape ifTrue: [ - self closeMenu. - ^ true ]. + self closeMenu. + ^ true ]. + ^ false ] diff --git a/src/NECompletion-Morphic/NECMenuMorph.class.st b/src/NECompletion-Morphic/NECMenuMorph.class.st index d155e76779b..9efef20967a 100644 --- a/src/NECompletion-Morphic/NECMenuMorph.class.st +++ b/src/NECompletion-Morphic/NECMenuMorph.class.st @@ -487,21 +487,31 @@ NECMenuMorph >> initialize [ isDetailMorphVisible := false ] -{ #category : 'actions' } +{ #category : 'drawing' } NECMenuMorph >> insertSelected [ - | selectionIndex | - self delete. + | selectionIndex selectedEntry | context hasEntries ifFalse: [ ^ false ]. - context activateEntryAt: self selectedIndex. - - "Now we can handle the announcement" + selectionIndex := self selectedIndex. - self class environment codeChangeAnnouncer announce: (CompletionItemSelected new - selectedItem: (context entries at: selectionIndex); - token: context completionToken; - entries: context entries; - index: selectionIndex). + selectedEntry := context entries at: selectionIndex. + + selectedEntry isGroupEntry ifTrue: [ + selectedEntry activateOn: context. + self hideDetail. + self refreshSelection. + ^ true ]. + + self delete. + context activateEntryAt: selectionIndex. + + self class environment codeChangeAnnouncer announce: + (CompletionItemSelected new + selectedItem: selectedEntry; + token: context completionToken; + entries: context entries; + index: selectionIndex). + ^ true ] @@ -521,6 +531,15 @@ NECMenuMorph >> lastVisible [ ^ (self firstVisible + self height-1) min: (self itemsCount) ] +{ #category : 'drawing' } +NECMenuMorph >> leaveGroup [ + + (context leaveGroup) ifFalse: [ ^ false ]. + self hideDetail. + self refreshSelection. + ^ true +] + { #category : 'event handling' } NECMenuMorph >> mouseDown: evt [ (self bounds containsPoint: evt cursorPoint) diff --git a/src/NECompletion/NECEntry.class.st b/src/NECompletion/NECEntry.class.st index 3b6c0327a8a..124c17d4b4e 100644 --- a/src/NECompletion/NECEntry.class.st +++ b/src/NECompletion/NECEntry.class.st @@ -138,6 +138,12 @@ NECEntry >> hightlightSymbol [ ^nil ] +{ #category : 'accessing' } +NECEntry >> isGroupEntry [ + + ^ false +] + { #category : 'accessing' } NECEntry >> label [ diff --git a/src/NECompletion/NECPrefixGroupEntry.class.st b/src/NECompletion/NECPrefixGroupEntry.class.st new file mode 100644 index 00000000000..ba5a30c6edf --- /dev/null +++ b/src/NECompletion/NECPrefixGroupEntry.class.st @@ -0,0 +1,96 @@ +Class { + #name : 'NECPrefixGroupEntry', + #superclass : 'NECEntry', + #instVars : [ + 'children', + 'prefix' + ], + #category : 'NECompletion-Model', + #package : 'NECompletion', + #tag : 'Model' +} + +{ #category : 'as yet unclassified' } +NECPrefixGroupEntry class >> prefix: aString children: aCollection [ + + ^ self new + prefix: aString; + children: aCollection asOrderedCollection; + contents: aString; + displayedContents: aString , ' ▸'; + yourself +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> activateOn: aCompletionContext [ + + aCompletionContext openGroup: self +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> browse [ + + ^ false +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> children [ + + ^ children +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> children: aCollection [ + + children := aCollection +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> createDescription [ + + | preview limit | + limit := children size min: 10. + + preview := String streamContents: [ :stream | + 1 to: limit do: [ :i | + stream + nextPutAll: (children at: i) displayedContents; + cr ] ]. + + ^ NECEntryDescription + label: 'group' + title: prefix , ' (' , children size asString , ')' + description: preview +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> hightlightSymbol [ + + ^ children + ifEmpty: [ nil ] + ifNotEmpty: [ children first hightlightSymbol ] +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> isGroupEntry [ + + ^ true +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> prefix [ + + ^ prefix +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> prefix: aString [ + + prefix := aString +] + +{ #category : 'operations' } +NECPrefixGroupEntry >> rawLabel [ + + ^ 'group' +] From cdb5a4b10ac10da6a545718661426a3f2e190c98 Mon Sep 17 00:00:00 2001 From: Omar AbedelKader <113094232+omarabedelkader@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:44:51 +0100 Subject: [PATCH 2/2] adding the settings --- .../CoCompletionContext.class.st | 128 +++++++++++++-- .../NECPreferences.class.st | 152 ++++++++++++------ 2 files changed, 216 insertions(+), 64 deletions(-) diff --git a/src/HeuristicCompletion-Model/CoCompletionContext.class.st b/src/HeuristicCompletion-Model/CoCompletionContext.class.st index b56fdd89e5a..d9d1fb42d79 100644 --- a/src/HeuristicCompletion-Model/CoCompletionContext.class.st +++ b/src/HeuristicCompletion-Model/CoCompletionContext.class.st @@ -14,7 +14,10 @@ Class { 'completionBuilder', 'completionClass', 'completion', - 'completionToken' + 'completionToken', + 'groupStack', + 'groupedEntries', + 'rawEntries' ], #category : 'HeuristicCompletion-Model-SystemIntegration', #package : 'HeuristicCompletion-Model', @@ -32,12 +35,45 @@ CoCompletionContext class >> engine: aCompletionEngine class: aClass source: aSt yourself ] -{ #category : 'activation' } +{ #category : 'accessing' } CoCompletionContext >> activateEntryAt: anIndex [ (self entries at: anIndex) activateOn: self ] +{ #category : 'private-grouping' } +CoCompletionContext >> buildGroupedEntriesFrom: aCollection [ + + + | grouped order buckets | + NECPreferences groupEntries ifFalse: [ ^ aCollection asOrderedCollection ]. + grouped := OrderedCollection new. + order := OrderedCollection new. + buckets := Dictionary new. + + aCollection do: [ :entry | + | prefix bucket | + prefix := self groupingPrefixFor: entry. + prefix + ifNil: [ grouped add: entry ] + ifNotNil: [ + bucket := buckets + at: prefix + ifAbsentPut: [ + order add: prefix. + OrderedCollection new ]. + bucket add: entry ] ]. + + order do: [ :prefix | + | bucket | + bucket := buckets at: prefix. + bucket size = 1 + ifTrue: [ grouped add: bucket first ] + ifFalse: [ grouped add: (NECPrefixGroupEntry prefix: prefix children: bucket) ] ]. + + ^ grouped +] + { #category : 'testing' } CoCompletionContext >> completion [ @@ -125,11 +161,21 @@ CoCompletionContext >> engine: anObject [ { #category : 'accessing' } CoCompletionContext >> entries [ - ^ self completion first: 20 + | currentEntries limit | + currentEntries := (groupStack notNil and: [ groupStack notEmpty ]) + ifTrue: [ groupStack last ] + ifFalse: [ + groupedEntries ifNil: [ + groupedEntries := self buildGroupedEntriesFrom: self rawEntries ]. + groupedEntries ]. + + limit := currentEntries size min: 20. + ^ currentEntries first: limit ] { #category : 'accessing' } CoCompletionContext >> entryCount [ + ^ self entries size ] @@ -139,23 +185,41 @@ CoCompletionContext >> environmentAt: aString ifPresent: aBlockClosure [ ^ self systemNavigation environmentAt: aString ifPresent: aBlockClosure ] -{ #category : 'testing' } +{ #category : 'accessing' } +CoCompletionContext >> groupingPrefixFor: anEntry [ + + | text prefixSize | + text := anEntry contents. + prefixSize := self groupingPrefixSize max: self completionToken size. + + text size <= prefixSize ifTrue: [ ^ nil ]. + ^ text copyFrom: 1 to: prefixSize +] + +{ #category : 'private-grouping' } +CoCompletionContext >> groupingPrefixSize [ + + ^ NECPreferences groupingPrefixSize +] + +{ #category : 'accessing' } CoCompletionContext >> hasEntries [ - ^ self completion notEmpty +^ self entries notEmpty ] -{ #category : 'menu' } +{ #category : 'accessing' } CoCompletionContext >> hasMessage [ - ^ false + ^ (groupStack notNil and: [ groupStack notEmpty ]) ] -{ #category : 'initialization' } +{ #category : 'accessing' } CoCompletionContext >> initialize [ super initialize. - completionBuilder := CoASTHeuristicsResultSetBuilder initializeOnContext: self + completionBuilder := CoASTHeuristicsResultSetBuilder initializeOnContext: self. + groupStack := OrderedCollection new. ] { #category : 'testing' } @@ -164,11 +228,40 @@ CoCompletionContext >> isScripting [ ^ engine isNotNil and: [ engine isScripting ] ] -{ #category : 'narrowing' } +{ #category : 'accessing' } +CoCompletionContext >> leaveGroup [ + + groupStack isEmpty ifTrue: [ ^ false ]. + groupStack removeLast. + ^ true +] + +{ #category : 'accessing' } +CoCompletionContext >> message [ + groupStack isEmpty ifTrue: [ ^ '' ]. + ^ ' back • ' + , (groupStack last size asString) + , ' matches for ' + , groupStack last first contents +] + +{ #category : 'accessing' } CoCompletionContext >> narrowWith: aString [ completionToken := aString. - self completion replaceFilterWith: (CoBeginsWithFilter caseSensitive: NECPreferences caseSensitive filterString: aString) + groupStack removeAll. + groupedEntries := nil. + rawEntries := nil. + self completion replaceFilterWith: + (CoBeginsWithFilter + caseSensitive: NECPreferences caseSensitive + filterString: aString) +] + +{ #category : 'accessing' } +CoCompletionContext >> openGroup: aGroupEntry [ + + groupStack add: aGroupEntry children ] { #category : 'accessing' } @@ -181,6 +274,15 @@ CoCompletionContext >> position: anObject [ position := anObject ] +{ #category : 'accessing' } +CoCompletionContext >> rawEntries [ + + rawEntries ifNil: [ + self completion fetchAll. + rawEntries := self completion results asOrderedCollection ]. + ^ rawEntries +] + { #category : 'replacing' } CoCompletionContext >> replaceTokenInEditorWith: aString [ @@ -206,7 +308,9 @@ CoCompletionContext >> systemNavigation [ { #category : 'accessing' } CoCompletionContext >> title [ - ^ '' + ^ groupStack isEmpty + ifTrue: [ '' ] + ifFalse: [ 'Completions · ' , groupStack last first contents ] ] { #category : 'accessing' } diff --git a/src/NECompletion-Preferences/NECPreferences.class.st b/src/NECompletion-Preferences/NECPreferences.class.st index 8dd07274be8..fb13deb7ff1 100644 --- a/src/NECompletion-Preferences/NECPreferences.class.st +++ b/src/NECompletion-Preferences/NECPreferences.class.st @@ -20,7 +20,9 @@ Class { 'popupShowWithShortcut', 'smartCharactersWithSingleSpace', 'smartCharactersWithDoubleSpace', - 'showCompletionDetails' + 'showCompletionDetails', + 'groupEntries', + 'groupingPrefixSize' ], #category : 'NECompletion-Preferences', #package : 'NECompletion-Preferences' @@ -66,6 +68,17 @@ NECPreferences class >> defaultBackgroundColor [ ^ self theme menuColor ] +{ #category : 'defaults' } +NECPreferences class >> defaultGroupEntries [ + ^ true +] + +{ #category : 'defaults' } +NECPreferences class >> defaultGroupingPrefixSize [ + + ^ 5 +] + { #category : 'defaults' } NECPreferences class >> defaultPopupDelay [ ^ 200 @@ -117,8 +130,33 @@ NECPreferences class >> expandPrefixes: aBoolean [ expandPrefixes := aBoolean ] +{ #category : 'accessing' } +NECPreferences class >> groupEntries [ + + ^ groupEntries +] + +{ #category : 'accessing' } +NECPreferences class >> groupEntries: aBoolean [ + + groupEntries := aBoolean +] + +{ #category : 'accessing' } +NECPreferences class >> groupingPrefixSize [ + + ^ groupingPrefixSize +] + +{ #category : 'accessing' } +NECPreferences class >> groupingPrefixSize: anInteger [ + + groupingPrefixSize := anInteger +] + { #category : 'class initialization' } NECPreferences class >> initialize [ + enabled := true. caseSensitive := true. smartCharacters := true. @@ -141,6 +179,8 @@ NECPreferences class >> initialize [ popupShowAutomatic := self defaultPopupShowAutomatic. popupAutomaticDelay := self defaultPopupDelay. showCompletionDetails := self defaultShowCompletionDetails. + groupEntries := self defaultGroupEntries. + groupingPrefixSize := self defaultGroupingPrefixSize. NECEntry spaceAfterCompletion: true ] @@ -176,6 +216,7 @@ NECPreferences class >> popupShowWithShortcut: anObject [ { #category : 'settings' } NECPreferences class >> settingsOn: aBuilder [ + (aBuilder setting: #'Code Completion') target: self; @@ -185,10 +226,9 @@ NECPreferences class >> settingsOn: aBuilder [ iconName: #smallConfiguration; description: 'Enable or disable code completion in browsers, debuggers and workspaces.'; with: [ - | availableControllers availableSorters | - availableControllers := self availableEngines. - availableControllers size > 1 - ifTrue: [ + | availableControllers availableSorters | + availableControllers := self availableEngines. + availableControllers size > 1 ifTrue: [ (aBuilder pickOne: #completionEngineClass) order: -1; label: 'CompletionEngine'; @@ -196,8 +236,7 @@ NECPreferences class >> settingsOn: aBuilder [ getSelector: #completionEngineClass; setSelector: #completionEngineClass:; default: (Smalltalk at: #CoCompletionEngine); - domainValues: availableControllers ]. - "availableSorters := self availableSorters. + domainValues: availableControllers ]. "availableSorters := self availableSorters. availableSorters size > 1 ifTrue: [ (aBuilder pickOne: #completionSorter) @@ -207,51 +246,60 @@ NECPreferences class >> settingsOn: aBuilder [ getSelector: #sorterClass; setSelector: #sorterClass:; domainValues: availableSorters ]." - (aBuilder setting: #backgroundColor) - default: (Color - r: 0.823069403714565 - g: 0.823069403714565 - b: 0.823069403714565 alpha: 1.0); - label: 'Background Color'. - (aBuilder setting: #useEnterToAccept) - label: 'Use ENTER to accept a suggested completion'; - default: self defaultUseEnterToAccept. - (aBuilder setting: #showCompletionDetails) - label: 'Show completion details'; - description: 'Show detailed information about completion entries'; - default: self defaultShowCompletionDetails. - (aBuilder setting: #caseSensitive) - label: 'Case Sensitive'; - default: true; - description: 'Decide if you want eCompletion to be case sensitive or not.'. - (aBuilder setting: #smartCharacters) - label: 'Smart Characters'; - default: true; - description: 'Decide if you want eCompletion to use smart characters, e.g, to automatically close brackets.'. - (aBuilder setting: #smartCharactersWithSingleSpace) - label: 'Smart Characters with Single Space'; - default: ''; - description: 'Enumerate the characters which are automatically inserted with a single space in between.'. - (aBuilder setting: #smartCharactersWithDoubleSpace) - label: 'Smart Characters with Double Space'; - default: '[]{}'; - description: 'Enumerate the characters which are automatically inserted with a two spaces in between.'. - (aBuilder setting: #popupShowAutomatic) - default: self defaultPopupShowAutomatic; - label: 'Popup is automatic'. - (aBuilder setting: #popupAutomaticDelay) - default: self defaultPopupDelay; - label: 'Popup appearance delay'. - (aBuilder pickOne: #popupShowWithShortcut) - target: self; - default: self defaultPopupShortcut; - label: 'Popup appears with this shortcut'; - domainValues: self availablePopupShortcuts. - (aBuilder setting: #spaceAfterCompletion) - target: NECEntry; - default: self defaultSpaceAfterCompletion; - label: 'Put a space after completion' - ] + (aBuilder setting: #backgroundColor) + default: (Color + r: 0.823069403714565 + g: 0.823069403714565 + b: 0.823069403714565 + alpha: 1.0); + label: 'Background Color'. + (aBuilder setting: #useEnterToAccept) + label: 'Use ENTER to accept a suggested completion'; + default: self defaultUseEnterToAccept. + (aBuilder setting: #showCompletionDetails) + label: 'Show completion details'; + description: 'Show detailed information about completion entries'; + default: self defaultShowCompletionDetails. + (aBuilder setting: #groupEntries) + label: 'Group completion entries'; + default: self defaultGroupEntries; + description: 'Group completion results by common prefix.'. + + (aBuilder setting: #groupingPrefixSize) + label: 'Grouping prefix size'; + default: self defaultGroupingPrefixSize; + description: 'Number of leading characters used to build completion groups.'. + (aBuilder setting: #caseSensitive) + label: 'Case Sensitive'; + default: true; + description: 'Decide if you want eCompletion to be case sensitive or not.'. + (aBuilder setting: #smartCharacters) + label: 'Smart Characters'; + default: true; + description: 'Decide if you want eCompletion to use smart characters, e.g, to automatically close brackets.'. + (aBuilder setting: #smartCharactersWithSingleSpace) + label: 'Smart Characters with Single Space'; + default: ''; + description: 'Enumerate the characters which are automatically inserted with a single space in between.'. + (aBuilder setting: #smartCharactersWithDoubleSpace) + label: 'Smart Characters with Double Space'; + default: '[]{}'; + description: 'Enumerate the characters which are automatically inserted with a two spaces in between.'. + (aBuilder setting: #popupShowAutomatic) + default: self defaultPopupShowAutomatic; + label: 'Popup is automatic'. + (aBuilder setting: #popupAutomaticDelay) + default: self defaultPopupDelay; + label: 'Popup appearance delay'. + (aBuilder pickOne: #popupShowWithShortcut) + target: self; + default: self defaultPopupShortcut; + label: 'Popup appears with this shortcut'; + domainValues: self availablePopupShortcuts. + (aBuilder setting: #spaceAfterCompletion) + target: NECEntry; + default: self defaultSpaceAfterCompletion; + label: 'Put a space after completion' ] ] { #category : 'accessing' }