Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/HeuristicCompletion-Model/CoBeginsWithFilter.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,73 @@ CoBeginsWithFilter >> completionString: anObject [
completionString := anObject
]

{ #category : 'testing' }
CoBeginsWithFilter >> fuzzyAcceptsContents: aString [

^ (self fuzzyScoreForContents: aString) isNotNil
]

{ #category : 'testing' }
CoBeginsWithFilter >> fuzzyScoreForContents: aString [

^ CoFuzzyMatcher
scoreForCandidate: aString
token: completionString
caseSensitive: self isCaseSensitive
tolerance: CoCompletionTolerance rate
]

{ #category : 'testing' }
CoBeginsWithFilter >> isCaseSensitive [

self subclassResponsibility
]

{ #category : 'testing' }
CoBeginsWithFilter >> isLessNarrowThanNegation: anotherFilter [

^ false
]

{ #category : 'testing' }
CoBeginsWithFilter >> prefixAcceptsContents: aString [

self subclassResponsibility
]

{ #category : 'testing' }
CoBeginsWithFilter >> requiresFallbackEnumeration [

^ CoCompletionTolerance enabled
and: [ completionString size >= CoCompletionTolerance minimumTokenSize ]
]

{ #category : 'testing' }
CoBeginsWithFilter >> sortEntries: aCollection [

| exact fuzzy |
self requiresFallbackEnumeration ifFalse: [ ^ aCollection ].

exact := OrderedCollection new.
fuzzy := OrderedCollection new.

aCollection do: [ :each |
(self prefixAcceptsContents: each contents)
ifTrue: [ exact add: each ]
ifFalse: [ fuzzy add: each ] ].

fuzzy isEmpty ifTrue: [ ^ aCollection ].

fuzzy sort: [ :a :b |
| scoreA scoreB |
scoreA := self fuzzyScoreForContents: a contents.
scoreB := self fuzzyScoreForContents: b contents.
scoreA = scoreB
ifTrue: [ a contents <= b contents ]
ifFalse: [ scoreA < scoreB ] ].

^ OrderedCollection new
addAll: exact;
addAll: fuzzy;
yourself
]
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ CoCaseInsensitiveBeginsWithFilter class >> filterString: aString [
{ #category : 'testing' }
CoCaseInsensitiveBeginsWithFilter >> accepts: aCandidate [

^ aCandidate contents asLowercase beginsWith: completionString asLowercase
^ (self prefixAcceptsContents: aCandidate contents)
or: [ CoCompletionTolerance enabled
and: [ self fuzzyAcceptsContents: aCandidate contents ] ]
]

{ #category : 'testing' }
CoCaseInsensitiveBeginsWithFilter >> isCaseSensitive [

^ false
]

{ #category : 'testing' }
Expand All @@ -42,3 +50,9 @@ CoCaseInsensitiveBeginsWithFilter >> isMoreNarrowThan: anotherFilter [

^ anotherFilter isLessNarrowThanCaseInsensitive: self
]

{ #category : 'testing' }
CoCaseInsensitiveBeginsWithFilter >> prefixAcceptsContents: aString [

^ aString asLowercase beginsWith: completionString asLowercase
]
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ CoCaseSensitiveBeginsWithFilter class >> filterString: aString [
{ #category : 'testing' }
CoCaseSensitiveBeginsWithFilter >> accepts: aCandidate [

^ aCandidate contents beginsWith: completionString
^ (self prefixAcceptsContents: aCandidate contents)
or: [ CoCompletionTolerance enabled
and: [ self fuzzyAcceptsContents: aCandidate contents ] ]
]

{ #category : 'testing' }
CoCaseSensitiveBeginsWithFilter >> isCaseSensitive [

^ true
]

{ #category : 'testing' }
Expand All @@ -42,3 +50,9 @@ CoCaseSensitiveBeginsWithFilter >> isMoreNarrowThan: anotherFilter [

^ anotherFilter isLessNarrowThanCaseSensitive: self
]

{ #category : 'testing' }
CoCaseSensitiveBeginsWithFilter >> prefixAcceptsContents: aString [

^ aString beginsWith: completionString
]
53 changes: 53 additions & 0 deletions src/HeuristicCompletion-Model/CoCompletionTolerance.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"
Stores the runtime settings for fuzzy completion, such as the allowed typo rate and the minimum token size.

"
Class {
#name : 'CoCompletionTolerance',
#superclass : 'Object',
#classInstVars : [
'minimumTokenSize',
'rate'
],
#category : 'HeuristicCompletion-Model-Core',
#package : 'HeuristicCompletion-Model',
#tag : 'Core'
}

{ #category : 'accessing' }
CoCompletionTolerance class >> enabled [

^ self rate > 0.0
]

{ #category : 'accessing' }
CoCompletionTolerance class >> minimumTokenSize [
^ minimumTokenSize ifNil: [ minimumTokenSize := 2 ]
]

{ #category : 'accessing' }
CoCompletionTolerance class >> minimumTokenSize: anInteger [
minimumTokenSize := (anInteger ifNil: [ 4 ]) max: 1
]

{ #category : 'accessing' }
CoCompletionTolerance class >> rate [

^ rate ifNil: [ rate := 0.0 ]
]

{ #category : 'accessing' }
CoCompletionTolerance class >> rate: aNumber [

| value |
value := (aNumber ifNil: [ 0.0 ]) asFloat.
rate := (value max: 0.0) min: 1.0
]

{ #category : 'accessing' }
CoCompletionTolerance class >> reset [

<script>
rate := 0.0.
minimumTokenSize := 2
]
6 changes: 6 additions & 0 deletions src/HeuristicCompletion-Model/CoFilter.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ CoFilter >> negated [
negatedFilter: self;
yourself
]

{ #category : 'sorting' }
CoFilter >> sortEntries: aCollection [

^ aCollection
]
111 changes: 111 additions & 0 deletions src/HeuristicCompletion-Model/CoFuzzyMatcher.class.st
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"
Computes fuzzy match scores between the typed token and a candidate using Damerau-Levenshtein distance

"
Class {
#name : 'CoFuzzyMatcher',
#superclass : 'Object',
#category : 'HeuristicCompletion-Model-Core',
#package : 'HeuristicCompletion-Model',
#tag : 'Core'
}

{ #category : 'matching' }
CoFuzzyMatcher class >> damerauLevenshteinDistanceBetween: source and: target maxDistance: maxDistance [

| previousRow currentRow twoRowsBack minInRow cost insertion deletion substitution value |
source = target ifTrue: [ ^ 0 ].

previousRow := Array new: target size + 1.
1 to: (target size + 1) do: [ :j |
previousRow at: j put: j - 1 ].
twoRowsBack := nil.

1 to: source size do: [ :i |
currentRow := Array new: target size + 1.
currentRow at: 1 put: i.
minInRow := i.

1 to: target size do: [ :j |
cost := ((source at: i) = (target at: j))
ifTrue: [ 0 ]
ifFalse: [ 1 ].

insertion := (currentRow at: j) + 1.
deletion := (previousRow at: (j + 1)) + 1.
substitution := (previousRow at: j) + cost.

value := insertion min: deletion.
value := value min: substitution.

(i > 1 and: [ j > 1 and: [
(source at: i) = (target at: (j - 1))
and: [ (source at: (i - 1)) = (target at: j) ] ] ]) ifTrue: [
twoRowsBack ifNotNil: [
value := value min: ((twoRowsBack at: (j - 1)) + 1) ] ].

currentRow at: (j + 1) put: value.
minInRow := minInRow min: value ].

minInRow > maxDistance ifTrue: [ ^ nil ].
twoRowsBack := previousRow.
previousRow := currentRow ].

^ (previousRow last <= maxDistance)
ifTrue: [ previousRow last ]
ifFalse: [ nil ]
]

{ #category : 'matching' }
CoFuzzyMatcher class >> scoreForCandidate: aCandidateString token: aTokenString caseSensitive: aBoolean tolerance: aTolerance [
| candidate token maxSize maxEdits distance |
candidate := aCandidateString asString.
token := aTokenString asString.

token isEmpty ifTrue: [ ^ 0.0 ].
candidate = token ifTrue: [ ^ 0.0 ].

candidate := aBoolean
ifTrue: [ candidate ]
ifFalse: [ candidate asLowercase ].
token := aBoolean
ifTrue: [ token ]
ifFalse: [ token asLowercase ].

(candidate beginsWith: token) ifTrue: [ ^ 0.0 ].
(self shouldTryFuzzyFor: token candidate: candidate tolerance: aTolerance) ifFalse: [ ^ nil ].

maxSize := token size max: candidate size.
maxEdits := (aTolerance * maxSize) ceiling.
maxEdits <= 0 ifTrue: [ ^ nil ].

distance := self
damerauLevenshteinDistanceBetween: token
and: candidate
maxDistance: maxEdits.

distance ifNil: [ ^ nil ].
^ distance asFloat / maxSize
]

{ #category : 'matching' }
CoFuzzyMatcher class >> shouldTryFuzzyFor: token candidate: candidate tolerance: aTolerance [

| maxSize maxEdits |
(CoCompletionTolerance enabled not or: [ aTolerance <= 0.0 ]) ifTrue: [ ^ false ].
token isEmpty ifTrue: [ ^ false ].
candidate isEmpty ifTrue: [ ^ false ].
token size < CoCompletionTolerance minimumTokenSize ifTrue: [ ^ false ].

"Keep keyword arity stable"
((token includes: $:) or: [ candidate includes: $: ]) ifTrue: [
(token occurrencesOf: $:) = (candidate occurrencesOf: $:)
ifFalse: [ ^ false ] ].

"Cheap guardrails to avoid noisy matches"
token first = candidate first ifFalse: [ ^ false ].

maxSize := token size max: candidate size.
maxEdits := (aTolerance * maxSize) ceiling.
^ (token size - candidate size) abs <= maxEdits
]
35 changes: 18 additions & 17 deletions src/HeuristicCompletion-Model/CoGlobalSelectorFetcher.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,24 @@ CoGlobalSelectorFetcher >> entriesDo: aBlock [
"

astNode ifNotNil: [ :node |
node parent ifNotNil: [ :parent | "Try first to continue the parent keyword message"
(parent isMessage and: [ parent isKeyword ]) ifTrue: [

self systemNavigation
allSelectorsStartingWith: parent selector , filter completionString
do: [ :e |
aBlock value: (NECSelectorEntry new
displayedContents:
'(' , (e copyFrom: 1 to: parent selector size) , ')' ,
(e copyFrom: parent selector size + 1 to: e size);
contents: (e copyFrom: parent selector size + 1 to: e size);
node: astNode;
fetcher: self;
selector: e) ] ] ] ]. "Otherwise, just go wide"
node parent ifNotNil: [ :parent |
(parent isMessage and: [ parent isKeyword ]) ifTrue: [
self systemNavigation
allSelectorsStartingWith: parent selector , filter completionString
do: [ :e |
aBlock value: (NECSelectorEntry new
displayedContents:
'(' , (e copyFrom: 1 to: parent selector size) , ')',
(e copyFrom: parent selector size + 1 to: e size);
contents: (e copyFrom: parent selector size + 1 to: e size);
node: astNode;
fetcher: self;
selector: e) ] ] ] ].

self systemNavigation allSelectorsStartingWith: filter completionString do: [ :e |
self systemNavigation
allSelectorsStartingWith: filter completionString
do: [ :e |
aBlock value: ((NECSelectorEntry contents: e node: astNode)
fetcher: self;
yourself) ]
fetcher: self;
yourself) ]
]
10 changes: 6 additions & 4 deletions src/HeuristicCompletion-Model/CoGlobalVariableFetcher.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ CoGlobalVariableFetcher >> entriesDo: aBlock [

self systemNavigation
allGlobalNamesStartingWith: filter completionString
do: [ :e | aBlock value:
((NECGlobalEntry contents: e node: astNode)
fetcher: self; yourself) ]
caseSensitive: (NECPreferences caseSensitive)
do: [ :e |
aBlock value: ((NECGlobalEntry contents: e node: astNode)
fetcher: self;
yourself) ]
caseSensitive: NECPreferences caseSensitive

]
4 changes: 2 additions & 2 deletions src/HeuristicCompletion-Model/CoResultSet.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ CoResultSet >> fetch: anInteger [

| newResults |
newResults := fetcher next: anInteger.
results addAll: "(sorter sortCompletionList:" newResults
results addAll: (filter sortEntries: newResults)
]

{ #category : 'fetching' }
CoResultSet >> fetchAll [

results addAll: fetcher upToEnd
results addAll: (filter sortEntries: fetcher upToEnd)
]

{ #category : 'accessing' }
Expand Down
Loading