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

[Backport to P12] Improve word-by-word text navigation + tests #17285

Open
wants to merge 1 commit into
base: Pharo12
Choose a base branch
from
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
157 changes: 149 additions & 8 deletions src/Rubric-Tests/RubTextEditorTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Class {
RubTextEditorTest >> setUp [

super setUp.
editor := RubTextEditor forTextArea: RubTextFieldArea new.
editor := RubTextEditor forTextArea: RubEditingArea new.
"Add text with a paragraph"
string := 'Lorem ipsum '.
editor addString: string
Expand Down Expand Up @@ -69,10 +69,7 @@ RubTextEditorTest >> testNextWord [
Should be: Lorem| ipsum"
self assert: (editor nextWord: i) equals: 6 ].

"Lorem| ipsum >> Lorem |ipsum"
self assert: (editor nextWord: 6) equals: 7.

7 to: 11 do: [ :i |
6 to: 11 do: [ :i |
"From: Lorem |ipsum
To: Lorem ipsu|m
Should be: Lorem ipsum|"
Expand All @@ -85,6 +82,72 @@ RubTextEditorTest >> testNextWord [
self assert: (editor nextWord: 999) equals: textSize + 1. "Out of range"
]

{ #category : 'tests' }
RubTextEditorTest >> testNextWordAtEndOfLineGoesToStartOfFirstWordOfNextLine [

editor addString: 'a
a
a'.

self assert: (editor nextWord: 2) equals: 4
]

{ #category : 'tests' }
RubTextEditorTest >> testNextWordAtEndOfLineGoesToStartOfNextLine [

editor addString: 'a

a'.

self assert: (editor nextWord: 2) equals: 3
]

{ #category : 'tests' }
RubTextEditorTest >> testNextWordAtLineWithSpacesGoesToEndOfCurrentLine [

editor addString: 'a

a'.

self assert: (editor nextWord: 2) equals: 4
]

{ #category : 'tests' }
RubTextEditorTest >> testNextWordAtStartOfEmptyLineGoesToStartOfNextLine [

editor addString: 'a

a'.

self assert: (editor nextWord: 3) equals: 4
]

{ #category : 'tests' }
RubTextEditorTest >> testNextWordAtStartOfFirstWordGoesToEndOfLine [

editor addString: 'a

a'.

self assert: (editor nextWord: 4) equals: 5
]

{ #category : 'tests' }
RubTextEditorTest >> testNextWordGoesToEndOfCurrentWord [

editor addString: 'abc d'.

self assert: (editor nextWord: 3) equals: 4
]

{ #category : 'tests' }
RubTextEditorTest >> testNextWordSkipsCurrentSpacesAndGoesToEndOfCurrentWord [

editor addString: 'abc d'.

self assert: (editor nextWord: 4) equals: 6
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWord [

Expand All @@ -99,7 +162,7 @@ RubTextEditorTest >> testPreviousWord [
Should be: Lorem| ipsum"
self assert: (editor previousWord: i) equals: 1 ].

8 to: 13 do: [ :i |
8 to: 12 do: [ :i |
"From: Lorem |ipsum
To: Lorem ipsu|m
Should be: Lorem ipsum|"
Expand All @@ -108,6 +171,84 @@ RubTextEditorTest >> testPreviousWord [
self assert: (editor previousWord: 999) equals: 7. "Out of range"
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWordAtEmptyLineGoesToEndOfPreviousLine [

editor addString: 'a

a'.

self assert: (editor previousWord: 3) equals: 2
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWordAtEndOfFirstWordOfLineGoesToStartOfLine [

editor addString: 'a

a'.

self assert: (editor previousWord: 5) equals: 4
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWordAtLineWithSpacesGoesToStartOfCurrentLine [

editor addString: 'a

a'.

self assert: (editor previousWord: 6) equals: 4
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWordAtStartOfLineGoesToEndOfLastWordOfPreviousLine [

editor addString: 'a
a'.

self assert: (editor previousWord: 4) equals: 2
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWordAtStartOfLineGoesToPreviousLine [

editor addString: 'a

a'.

self assert: (editor previousWord: 4) equals: 3
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWordGoesToStartOfCurrentWord [

editor addString: 'abc def'.

self assert: (editor previousWord: 6) equals: 5
]

{ #category : 'tests' }
RubTextEditorTest >> testPreviousWordSkipsCurrentSpacesAndGoesToStartOfCurrentWord [

editor addString: 'abc def'.

self assert: (editor previousWord: 5) equals: 1
]

{ #category : 'test-selection' }
RubTextEditorTest >> testSelectFromBeginToEnd [

editor addString: 'a

a'.

editor selectMark: 1 point: 5.
self assert: editor selection equals: 'a

a'.
]

{ #category : 'tests' }
RubTextEditorTest >> testSelectWord [

Expand Down Expand Up @@ -170,9 +311,9 @@ RubTextEditorTest >> testSelectWordMarkPoint [
self assert: editor markIndex equals: 7.
self assert: editor pointIndex equals: 12.

editor selectWordMark: 9 point: 12. "Lorem ipsum dolor sit amet >> Lorem [ipsum ]dolor sit amet "
editor selectWordMark: 9 point: 12. "Lorem ipsum dolor sit amet >> Lorem [ipsum dolor] sit amet "
self assert: editor markIndex equals: 7.
self assert: editor pointIndex equals: 13.
self assert: editor pointIndex equals: 18.

editor selectWordMark: 3 point: 24. "Lorem ipsum dolor sit amet >> [Lorem ipsum dolor sit amet ]"
self assert: editor markIndex equals: 1.
Expand Down
110 changes: 68 additions & 42 deletions src/Rubric/RubTextEditor.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,7 @@ RubTextEditor >> isTextEditor [
{ #category : 'private' }
RubTextEditor >> isWordCharacterAt: index in: aString [
"By default, group alphanumeric and non-alphanumeric separately"

^ (aString at: index) isAlphaNumeric
]

Expand Down Expand Up @@ -1606,20 +1607,26 @@ RubTextEditor >> moveCursor: directionBlock forward: forward specialBlock: speci
directionBlock is a one argument Block that computes the new Position from a given one.
specialBlock is a one argumentBlock that computes the new position from a given one under the alternate semantics.
Note that directionBlock always is evaluated first."
| shift indices newPosition |

| shift indices caretPosition newPosition |
shift := aKeyboardEvent shiftPressed.
indices := self setIndices: shift forward: forward.
newPosition := directionBlock value: (indices at: #moving).
(aKeyboardEvent commandKeyPressed or: [ aKeyboardEvent controlKeyPressed ])
ifTrue: [newPosition := specialBlock value: newPosition].
caretPosition := indices at: #moving.

newPosition := (aKeyboardEvent commandKeyPressed or: [
aKeyboardEvent controlKeyPressed ])
ifTrue: [ specialBlock value: caretPosition ]
ifFalse: [ directionBlock value: caretPosition ].

shift
ifTrue: [self selectMark: (indices at: #fixed) point: newPosition - 1]
ifFalse: [self selectAt: newPosition].
ifTrue: [
self selectMark: (indices at: #fixed) point: newPosition - 1 ]
ifFalse: [ self selectAt: newPosition ].
self setEmphasisHereFromTextForward: forward.

textArea
requestTextEditingAt: (textArea cursor positionInWorld
corner: textArea cursor positionInWorld)
textArea requestTextEditingAt:
(textArea cursor positionInWorld corner:
textArea cursor positionInWorld)
]

{ #category : 'nesting' }
Expand Down Expand Up @@ -1652,20 +1659,37 @@ RubTextEditor >> nextCharacterIfAbsent: aBlock [
{ #category : 'private' }
RubTextEditor >> nextWord: position [

| string index initialIsAlphaNumeric size |
| string index initialIsAlphaNumeric size initialPosition |
"Positions go from 1 to size + 1
Position N + 1 is after the Nth character"
index := 1 max: position.
initialPosition := 1 max: position.
index := initialPosition.
position traceCr.

string := self string.
size := string size + 1.
position >= size ifTrue: [ ^ size ].

initialIsAlphaNumeric := self isWordCharacterAt: index in: string.
[ index < string size and: [
{ Character space . Character tab }
includes: (string at: index)]
] whileTrue: [ index := index + 1 ].

[
index < size and: [
(self isWordCharacterAt: index in: string) = initialIsAlphaNumeric ] ]
initialIsAlphaNumeric := self isWordCharacterAt: index in: string.


index = initialPosition
ifTrue: [ index := index + 1 ].

[ | nextCharacterInDirection |
"If we reach the end"
index >= size ifTrue: [ ^ index ].

nextCharacterInDirection := string at: index.
nextCharacterInDirection = Character cr
ifTrue: [ ^ index ].

(self isWordCharacterAt: index in: string) = initialIsAlphaNumeric ]
whileTrue: [ index := index + 1 ].

^ index
Expand Down Expand Up @@ -1814,36 +1838,38 @@ RubTextEditor >> previousCharacterIfAbsent: aBlock [
{ #category : 'private' }
RubTextEditor >> previousWord: position [

| string index size beforeCaretIsAlpha afterCaretIsAlpha |
"The main strategy behind this is to move the caret backwards until there is a alpha/nonalpha or nonalpha/alpha pair.
To avoid staying in the same place, we need always move the character at least once before checking the condition"
| string index initialIsAlphaNumeric size initialPosition |
"Positions go from 1 to size + 1
Position N + 1 is after the Nth character"
string := self string.
size := string size + 1.

initialPosition := size min: position.
index := initialPosition.

position <= 2 ifTrue: [ ^ 1 ].
string := self string.
size := string size.

"If we ask for the word starting after the string, start at the end of the string.
Remember that there are N+1 caret positions for a N-sized string."
index := (size + 1) min: position.

"Go backwards once and check if character before the caret is alphanumeric.
If not alphanumeric, we go backwards once more.
This handles the case when the caret is after the first character of a word, in which case we should just go once backwards.
All other cases should go backwards more than once."
index := index - 1.
beforeCaretIsAlpha := index = 1 or: [
self isWordCharacterAt: index in: string ].
beforeCaretIsAlpha ifFalse: [
index := index - 1 ].

"Finally, iterate backwards until the alphanumericness before and after the caret changes"
[
afterCaretIsAlpha := beforeCaretIsAlpha := index = 1 or: [
self isWordCharacterAt: index in: string ].
beforeCaretIsAlpha := index = 1 or: [
self isWordCharacterAt: index - 1 in: string ].
beforeCaretIsAlpha = afterCaretIsAlpha and: [ index > 1 ] ]

[ index > 0 and: [
{ Character space . Character tab }
includes: (string at: index - 1)]
] whileTrue: [ index := index - 1 ].

initialIsAlphaNumeric := self isWordCharacterAt: index - 1 in: string.

index = initialPosition
ifTrue: [ index := index - 1 ].

[ | nextCharacterInDirection |
"If we reach the end"
index <= 1 ifTrue: [ ^ index ].

nextCharacterInDirection := string at: index - 1.
nextCharacterInDirection = Character cr
ifTrue: [ ^ index ].

(self isWordCharacterAt: index - 1 in: string) = initialIsAlphaNumeric ]
whileTrue: [ index := index - 1 ].

^ index
]

Expand Down