Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update word navigation to handle different character groups #17630

Merged
merged 4 commits into from
Jan 21, 2025
Merged
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
17 changes: 12 additions & 5 deletions src/Kernel/Character.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ Character class >> supportsNonASCII [
((self environment at: #EncodedCharSet) charsetAt: 255) name = #Unicode ]
]

{ #category : 'constants' }
Character class >> syntaxCharacters [

^ '^"''|[](){}$#<>=;:.' copy
]

{ #category : 'accessing - untypeable characters' }
Character class >> tab [
"Answer the Character representing a tab."
Expand Down Expand Up @@ -779,14 +785,15 @@ Character >> isSpecial [
]

{ #category : 'testing' }
Character >> isSpecialSyntaxValid [

^ '^"''[](){}#$:;.=' includes: self
Character >> isSurrogateOther [
^ self characterSet isSurrogateOther: self
]

{ #category : 'testing' }
Character >> isSurrogateOther [
^ self characterSet isSurrogateOther: self
Character >> isSyntax [
"Answer whether the receiver is part of the Smalltalk syntax, i.e. ^""'|[](){}$#<>=;:."

^ self class syntaxCharacters includes: self
]

{ #category : 'testing' }
Expand Down
71 changes: 54 additions & 17 deletions src/NECompletion-Morphic/CompletionEngine.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,20 @@ CompletionEngine >> completionToken [
{ #category : 'replacement' }
CompletionEngine >> completionTokenStart [
"This is the position in the editor where the completion token starts"

| caret |
self editor caret <= 1 ifTrue: [ ^ self editor caret ].
(self editor isWordCharacterAt: self editor caret - 1 in: self editor text) ifFalse: [^ self editor caret ].
^ self editor previousWord: self editor caret
(self editor
isWordCharacterAt: self editor caret - 1
in: self editor text) ifFalse: [ ^ self editor caret ].

"Look back.
If the caret is at the end of a keyword (i.e., just after a colon) go back twice"
caret := self editor caret.
(self editor characterAt: caret - 1) = $: ifTrue: [
caret := self editor previousWord: caret ].
caret := self editor previousWord: caret.
^ caret
]

{ #category : 'accessing' }
Expand Down Expand Up @@ -338,36 +349,54 @@ CompletionEngine >> replaceTokenInEditorWith: aString [

The completion context uses this API to insert text into the text editor"

| newString wordEnd doubleSpace wordStart oldSelectionInterval |
| newString doubleSpace oldSelectionInterval replacementInterval wordStart wordEnd |

newString := aString.
wordStart := self completionTokenStart.
oldSelectionInterval := self editor selectionInterval.
replacementInterval := self replacementInterval.
wordStart := replacementInterval first.
wordEnd := replacementInterval last.

"If completionToken is not empty, the end is the end of the whole word (according to the editor)"
wordEnd := wordStart = editor caret
ifTrue: [ wordStart ]
ifFalse: [ self editor nextWord: self editor caret - 1].
self editor
selectInvisiblyFrom: wordStart
to: wordEnd.

"Do not insert an aditional space if there is already one"
(newString last = $ and: [
wordEnd <= self editor text size and: [
(self editor text at: wordEnd) = $ ]]) ifTrue: [ newString := newString copyWithoutIndex: newString size ].
"If the returned index is the size of the text that means that the caret is at the end of the text and there is no more word after, so add 1 to the index to be out of range to select the entierely word because of the selectInvisiblyFrom:to: remove 1 just after to be at the end of then final word"
wordEnd > self editor text size ifTrue:[ wordEnd := wordEnd + 1 ].

oldSelectionInterval := self editor selectionInterval.
self editor
selectInvisiblyFrom: wordStart
to: wordEnd - 1.
(self editor text at: wordEnd + 1) = $ ]]) ifTrue: [ newString := newString copyWithoutIndex: newString size ].

self editor replaceSelectionWith: newString fromSelection: oldSelectionInterval.

doubleSpace := newString indexOfSubCollection: ' ' startingAt: 1 ifAbsent: [ newString size ].
self editor selectAt: wordStart + doubleSpace.

self editor morph invalidRect: self editor morph bounds
]

{ #category : 'replacement' }
CompletionEngine >> replacementInterval [

| wordEnd wordStart |
wordStart := self completionTokenStart.

"If completionToken is not empty, the end is the end of the whole word (according to the editor)"
wordEnd := wordStart = editor caret
ifTrue: [ wordStart ]
ifFalse: [ self editor nextWord: wordStart ].

"If this is a keyword (i.e., ends with a : and not a :=), replace the colon too, as we want to replace selector parts"
(wordEnd <= self editor text size and: [
(self editor characterAt: wordEnd) = $: ]) ifTrue: [
(wordEnd + 1 <= self editor text size and: [
(self editor characterAt: wordEnd + 1) = $= ]) ifFalse: [
wordEnd := wordEnd + 1 ] ].

"If the returned index is the size of the text that means that the caret is at the end of the text and there is no more word after, so add 1 to the index to be out of range to select the entierely word because of the selectInvisiblyFrom:to: remove 1 just after to be at the end of then final word"
wordEnd > self editor text size ifTrue: [ wordEnd := wordEnd + 1 ].

^ wordStart to: wordEnd - 1
]

{ #category : 'private' }
CompletionEngine >> resetCompletionDelay [
"Open the popup after 100ms and only after certain characters"
Expand Down Expand Up @@ -604,6 +633,14 @@ CompletionEngine >> stopCompletionDelay [
completionDelay isTerminating ifFalse: [ completionDelay terminate ] ]
]

{ #category : 'replacement' }
CompletionEngine >> tokenToReplace [

| interval |
interval := self replacementInterval.
^ self editor text copyFrom: interval first to: interval last
]

{ #category : 'keyboard' }
CompletionEngine >> updateCompletionAfterEdition: aParagraphEditor [

Expand Down
5 changes: 5 additions & 0 deletions src/NECompletion-Tests/CompletionEngineTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ CompletionEngineTest >> testCompletionAfterKeyword [

self setEditorTextWithCaret: 'self"x"foo:|"x"baz'.
self assert: controller completionToken equals: 'foo:'.
self assert: controller tokenToReplace equals: 'foo:'.
controller context replaceTokenInEditorWith: 'bar'.
self assert: self editorTextWithCaret equals: 'self"x"bar|"x"baz'.

Expand Down Expand Up @@ -198,6 +199,7 @@ CompletionEngineTest >> testCompletionAfterWord [

self setEditorTextWithCaret: 'self:=foo|:=baz'.
self assert: controller completionToken equals: 'foo'.
self assert: controller tokenToReplace equals: 'foo'.
controller context replaceTokenInEditorWith: 'bar'.
self assert: self editorTextWithCaret equals: 'self:=bar|:=baz'.

Expand Down Expand Up @@ -295,6 +297,7 @@ CompletionEngineTest >> testCompletionOnFirstLetter [

self setEditorTextWithCaret: 'self:=f|oo:=baz'.
self assert: controller completionToken equals: 'f'.
self assert: controller tokenToReplace equals: 'foo'.
controller context replaceTokenInEditorWith: 'bar'.
self assert: self editorTextWithCaret equals: 'self:=bar|:=baz'.

Expand Down Expand Up @@ -324,6 +327,7 @@ CompletionEngineTest >> testCompletionOnNoWord [

self setEditorTextWithCaret: 'self:=|:=baz'.
self assert: controller completionToken equals: ''.
self assert: controller tokenToReplace equals: ''.
controller context replaceTokenInEditorWith: 'bar'.
self assert: self editorTextWithCaret equals: 'self:=bar|:=baz'.

Expand Down Expand Up @@ -657,6 +661,7 @@ CompletionEngineTest >> testReplaceWithSpaces [

self setEditorTextWithCaret: 'self fo|o baz'.
self assert: controller completionToken equals: 'fo'.
self assert: controller tokenToReplace equals: 'foo'.
controller context replaceTokenInEditorWith: 'bar '.
self assert: self editorTextWithCaret equals: 'self bar| baz'.

Expand Down
Loading