'From Cuis 2.6 of 10 August 2010 [latest update: #540] on 11 August 2010 at 10:45:36 am'!

!Text commentStamp: '<historical>' prior: 0!
I represent a character string that has been marked with abstract changes in character appearance. Actual display is performed in the presence of a TextStyle which indicates, for each abstract code, an actual font to be used.  A Text associates a set of TextAttributes with each character in its character string.  These attributes may be font numbers, emphases such as bold or italic, or hyperling actions.  Font numbers are interpreted relative to whatever textStyle appears, along with the text, in a Paragraph.  Since most characters have the same attributes as their neighbors, the attributes are stored in a RunArray for efficiency.  Each of my instances has
	string		a String
	runs		a RunArray
	initialStyle	a TextStyle or nil. if nil, use TextStyle default
			
From the comment at #deepCopy, written by Dan on 11/9/97:
"Both string and runs are assumed to be read-only"!


!Clipboard methodsFor: 'accessing' stamp: 'jmv 8/11/2010 10:20'!
clipboardText: textOrString
	"Set text currently on the clipboard.  Also export to OS"
	| primitiveFormat |
	contents _ textOrString.
	self noteRecentClipping: textOrString asText.
	"Windows uses UTF-8 for clipboard. Other platforms use MacRoman"
	primitiveFormat _ Smalltalk platformName = 'Win32'
		ifTrue: [ textOrString asString latin1ToUtf8 ]
		ifFalse: [ textOrString asString latin1ToMacRoman ].
	self primitiveClipboardText: primitiveFormat! !


!Clipboard class methodsFor: 'accessing' stamp: 'jmv 8/11/2010 10:20'!
clipboardText: textOrString 
	^self default clipboardText: textOrString! !


!CodeHolder methodsFor: 'message list menu' stamp: 'jmv 8/11/2010 10:20'!
copyName
	"Copy the current selector to the clipboard"
	| selector |
	(selector _ self selectedMessageName) ifNotNil:
		[Clipboard clipboardText: selector asString ]! !


!DictionaryInspector methodsFor: 'menu' stamp: 'jmv 8/11/2010 10:20'!
copyName
	"Copy the name of the current variable, so the user can paste it into the 
	window below and work with is. If collection, do (xxx at: 1)."
	| sel |
	self selectionIndex <= self numberOfFixedFields
		ifTrue: [super copyName]
		ifFalse: [sel := String streamContents: [:strm | 
							strm nextPutAll: '(self at: '.
							(keyArray at: selectionIndex - self numberOfFixedFields)
								storeOn: strm.
							strm nextPutAll: ')'].
			Clipboard clipboardText: sel 			"no undo allowed"]! !


!Editor methodsFor: 'menu messages' stamp: 'jmv 8/11/2010 10:21'!
clipboardTextPut: textOrString

	^ Clipboard clipboardText: textOrString! !


!FileList methodsFor: 'menu messages' stamp: 'jmv 8/11/2010 10:21'!
copyName

	listIndex = 0 ifTrue: [^ self].
	Clipboard clipboardText: self fullName! !


!MessageNames methodsFor: 'message list menu' stamp: 'jmv 8/11/2010 10:21'!
copyName
	"Copy the current selector to the clipboard"

	| selector |
	(selector _ self selectorList at: selectorListIndex ifAbsent: nil) ifNotNil: [
		Clipboard clipboardText: selector asString ]! !


!Preference methodsFor: 'menu' stamp: 'jmv 8/11/2010 10:21'!
copyName
	"Copy the name of the given preference to the clipboard"

	Clipboard clipboardText: name asString! !


!PseudoClass methodsFor: 'class' stamp: 'jmv 8/11/2010 10:30'!
commentString: aString
	self classComment: aString! !


!SetInspector methodsFor: 'menu commands' stamp: 'jmv 8/11/2010 10:21'!
copyName
	"Copy the name of the current variable, so the user can paste it into the 
	window below and work with is. If collection, do (xxx at: 1)."
	| sel |
	self selectionIndex <= (2 + object class instSize)
		ifTrue: [super copyName]
		ifFalse: [sel := '(self array at: '
						, (String streamContents: 
							[:strm | self arrayIndexForSelection storeOn: strm]) , ')'.
			Clipboard clipboardText: sel ]! !


!Text methodsFor: 'accessing' stamp: 'jmv 8/10/2010 14:03'!
initialStyle

	^initialStyle ifNil: [ TextStyle default ]! !

!Text methodsFor: 'accessing' stamp: 'jmv 8/10/2010 14:06'!
initialStyleOrNil

	^initialStyle! !

!Text methodsFor: 'accessing' stamp: 'jmv 8/11/2010 09:46'!
replaceFrom: start to: stop with: replacement
	"newSize = oldSize - (stop-start-1) + aText size"
	
	| rep |
	rep _ replacement asText.	"might be a string"
	string _ string copyReplaceFrom: start to: stop with: rep string.
	runs _ runs copyReplaceFrom: start to: stop with: rep runs.
	"Ensure the ParagraphAttributes invariant for the interval that could have been affected"
	self fixParagraphAttributesFrom: start to: start + replacement size - 1! !

!Text methodsFor: 'converting' stamp: 'jmv 8/11/2010 09:46'!
replaceFrom: start to: stop with: replacement startingAt: repStart 
	"This destructively replaces elements from start to stop in the receiver starting at index, repStart, in replacementCollection. 
	Do it to both the string and the runs.
	The size does not change"

	| rep newRepRuns |
	rep _ replacement asText.	"might be a string"
	string replaceFrom: start to: stop with: rep string startingAt: repStart.
	newRepRuns _ rep runs copyFrom: repStart to: repStart + stop - start.
	runs _ runs copyReplaceFrom: start to: stop with: newRepRuns.
	"Ensure the ParagraphAttributes invariant for the interval that could have been affected"
	self fixParagraphAttributesFrom: start to: start + replacement size - 1! !

!Text methodsFor: 'emphasis' stamp: 'jmv 8/10/2010 16:07'!
addAttribute: att from: requestedStart to: requestedStop

	"Set the attribute for characters in the interval start to stop."
	| intervalToFix start stop |
	start _ requestedStart.
	stop _ requestedStop.
	
	"If att must be applied to whole paragraphs, do so."
	att isParagraphAttribute ifTrue: [
		intervalToFix _ self encompassParagraph: (start to: stop).
		start _ intervalToFix first.
		stop _ intervalToFix last ].

	runs _  runs
		copyReplaceFrom: start
		to: stop
		with: ((runs copyFrom: start to: stop) mapValues: [ :attributes | 
			Text addAttribute: att toArray: attributes]).
	runs coalesce! !

!Text methodsFor: 'emphasis' stamp: 'jmv 8/10/2010 16:06'!
removeAttribute: att from: requestedStart to: requestedStop
	"Remove the attribute over the interval start to stop."

	| intervalToFix start stop |
	start _ requestedStart.
	stop _ requestedStop.
	
	"If att must be applied to whole paragraphs, do so."
	att isParagraphAttribute ifTrue: [
		intervalToFix _ self encompassParagraph: (start to: stop).
		start _ intervalToFix first.
		stop _ intervalToFix last ].

	runs _ runs
		copyReplaceFrom: start
		to: stop
		with: ((runs copyFrom: start to: stop) mapValues: [ :attributes | 
			attributes copyWithout: att]).
	runs coalesce! !

!Text methodsFor: 'emphasis' stamp: 'jmv 8/10/2010 14:09'!
textStyleAt: characterIndex
	| textStyle |
	textStyle _ self initialStyle.
	self size = 0 ifTrue: [ ^ textStyle ].
	(runs at: characterIndex) do: [ :attribute | 
		attribute forTextStyleReferenceDo: [ :s | textStyle _ s ] ].
	^textStyle! !

!Text methodsFor: 'emphasis' stamp: 'jmv 8/10/2010 14:09'!
withAttributeValues: attributes do: aBlock
	"Evaluate aBlock with the values of various attributes applied in the correct order
	The order is (each overwriting the previous one)
	1) initialStyle
	2) TextStyleReferene
	3) CharacterStyleReference
	4) TextFontReference
	5) TextEmphasis"

	| textStyle characterStyle font emphasis alignment color kern |
	textStyle _ self initialStyle.	"First candidate as the textStyle"
	emphasis _ 0.
	kern _ 0.
	color _ nil.
	
	"TextStyle is the first to set several values"
	attributes do: [ :attribute |
		attribute forTextStyleReferenceDo: [ :s | 
			textStyle _ s]].
	font _ textStyle font.
	alignment _ textStyle alignment.
	textStyle color ifNotNil: [ :c | color _ c ].
	
	"CharacterStyle, if present, can override font and color"
	attributes do: [ :attribute |
		attribute forCharacterStyleReferenceDo: [ :s | 
			characterStyle _ s.
			font _ characterStyle font.
			characterStyle color ifNotNil: [ :c | color _ c ]]].
	
	"Hardcoded color for TextAction comes before TextColor"
	attributes do: [ :attribute |
		attribute forTextActionInfoDo: [ :info | color _ TextAction purple ]].

	"These will not interfere with each other, and all of them take precedence over previous values"
	attributes do: [ :attribute |
		attribute forFontReferenceDo: [ :f | font _ f ].
		attribute forTextEmphasisDo: [ :e | emphasis _ emphasis bitOr: e ].
		attribute forTextColorDo: [ :c | color _ c ].
		attribute forTextAlignmentDo: [ :a | alignment _ a ].
		attribute forTextKernDo: [ :k | kern _ kern + k ].
	].

	"Finally, the font must include any specified emphasis"
	font _ font emphasized: emphasis.
	
	"Done. Now evaluate the block."
	aBlock valueWithArguments: {font. color. alignment. textStyle. characterStyle. kern }! !

!Text methodsFor: 'private' stamp: 'jmv 8/10/2010 16:08'!
fixParagraphAttributesFrom: start to: end
	"Helper method to ensure the invariant that TextAttributes that answer true to
	 #isParagraphAttribute are only applied to whole paragraphs.
	See senders"
	
	| paragraphEnd paragraphInterval paragraphStart paragraphAttributes |
	
	paragraphEnd _ end max: start.	"end could be start-1 when new text is empty, for example, when backspacing"
	[
		paragraphInterval _ self encompassParagraph: (paragraphEnd to: paragraphEnd).
		paragraphStart _ paragraphInterval first.
		paragraphEnd _ paragraphInterval last.
		"We must honor the paragraph attributes as defined in the Character enter that ends the paragraph"
		paragraphAttributes _ (self attributesAt: paragraphEnd) select: [ :attr | attr isParagraphAttribute ].
		self privateSetParagraphAttributes: paragraphAttributes from: paragraphStart to: paragraphEnd.
	
		paragraphEnd _ paragraphStart - 1.
		paragraphStart > start ] whileTrue.
	runs coalesce! !

!Text methodsFor: 'attributes' stamp: 'jmv 8/10/2010 15:06'!
hasAnyAttribute
	"Return false if there are no emphasis (i.e., a String would not make a difference)"

	^initialStyle notNil or: [ runs values anySatisfy: [ :emphArray | emphArray notEmpty ]]! !


!Text class methodsFor: 'instance creation' stamp: 'jmv 8/10/2010 14:55'!
string: aString attributes: atts
	"Answer an instance of me whose characters are those of aString.
	atts is an array of TextAttributes."

	^self initialStyle: nil string: aString runs: (RunArray new: aString size withAll: atts)! !


!TextEditor methodsFor: 'accessing' stamp: 'jmv 8/11/2010 10:05'!
replaceSelectionWith: aTextOrString
	"Remember the selection text in UndoSelection.
	 Deselect, and replace the selection text by aText.
	 Remember the resulting selectionInterval in UndoInterval and PriorInterval.
	 Set up undo to use UndoReplace."

	beginTypeInBlock ifNotNil: [^self zapSelectionWith: aTextOrString]. "called from old code"
	UndoSelection _ self selection.
	self zapSelectionWith: aTextOrString.
	self undoer: #undoReplace! !

!TextEditor methodsFor: 'accessing' stamp: 'jmv 8/11/2010 10:14'!
setSearch: aStringOrText
	"Set the FindText and ChangeText to seek aString; except if already seeking aString, leave ChangeText alone so again will repeat last replacement."

	FindText = aStringOrText
		ifFalse: [FindText _ ChangeText _ aStringOrText]! !

!TextEditor methodsFor: 'editing keys' stamp: 'jmv 8/11/2010 10:05'!
changeLfToCr: aKeyboardEvent
	"Replace all LFs by CRs.
	Triggered by Cmd-U -- useful when getting code from FTP sites
	jmv- Modified to als change crlf by cr"
	
	| fixed |
	fixed _ self selection string.
	fixed _ fixed copyReplaceAll: String crlf with: String cr.
	fixed _ fixed copyReplaceAll: String lf with: String cr. 
	self replaceSelectionWith: fixed.
	^ true! !

!TextEditor methodsFor: 'editing keys' stamp: 'jmv 8/11/2010 10:33'!
copyHiddenInfo
	"In TextLinks, TextDoits, TextColor, and TextURLs, there is hidden
info.  Copy that to the clipboard.  You can paste it and see what it is.
Usually enclosed in <>."

	^ self clipboardTextPut: self hiddenInfo! !

!TextEditor methodsFor: 'editing keys' stamp: 'jmv 8/11/2010 10:34'!
inOutdent: aKeyboardEvent delta: delta
	"Add/remove a tab at the front of every line occupied by the selection. 
	Derived from work by Larry Tesler back in December 1985.  Now triggered by Cmd-L and Cmd-R.  2/29/96 sw"

	| cr realStart realStop lines startLine stopLine start stop adjustStart indentation size numLines inStream newString outStream |
	cr _ Character cr.

	"Operate on entire lines, but remember the real selection for re-highlighting later"
	realStart _ self startIndex.
	realStop _ self stopIndex - 1.

	"Special case a caret on a line of its own, including weird case at end of paragraph"
	(realStart > realStop and: [
				realStart < 2 or: [(paragraph string at: realStart - 1) == cr]])
		ifTrue:
			[delta < 0
				ifTrue: [
					morph flash]
				ifFalse: [
					self replaceSelectionWith: Character tab asSymbol.
					self selectAt: realStart + 1].
			^true].

	lines _ paragraph lines.
	startLine _ paragraph lineIndexOfCharacterIndex: realStart.
	stopLine _ paragraph lineIndexOfCharacterIndex: (realStart max: realStop).
	start _ (lines at: startLine) first.
	stop _ (lines at: stopLine) last.
	
	"Pin the start of highlighting unless the selection starts a line"
	adjustStart _ realStart > start.

	"Find the indentation of the least-indented non-blank line; never outdent more"
	indentation _ (startLine to: stopLine) inject: 1000 into: [ :previousValue :each |
		previousValue min: (paragraph indentationOfLineIndex: each ifBlank: [ :tabs | 1000 ])].			

	size _  stop + 1 - start.
	numLines _ stopLine + 1 - startLine.
	inStream _ ReadStream on: paragraph string from: start to: stop.

	newString _ String new: size + ((numLines * delta) max: 0).
	outStream _ ReadWriteStream on: newString.

	"This subroutine does the actual work"
	self indent: delta fromStream: inStream toStream: outStream.

	"Adjust the range that will be highlighted later"
	adjustStart ifTrue: [realStart _ (realStart + delta) max: start].
	realStop _ realStop + outStream position - size.

	"Prepare for another iteration"
	indentation _ indentation + delta.
	size _ outStream position.
	inStream _ outStream setFrom: 1 to: size.

	outStream
		ifNil: [ 	"tried to outdent but some line(s) were already left flush"
			morph flash]
		ifNotNil: [
			self selectInvisiblyFrom: start to: stop.
			size = newString size ifFalse: [newString _ outStream contents].
			self replaceSelectionWith: newString].
	self selectFrom: realStart to: realStop. 	"highlight only the original range"
	^ true! !

!TextEditor methodsFor: 'editing keys' stamp: 'jmv 8/11/2010 10:08'!
makeCapitalized: aKeyboardEvent
	"Force the current selection to uppercase.  Triggered by Cmd-X."

	| prev |
	prev _ $-.  "not a letter"
	self replaceSelectionWith: 
		(self selection string collect:
			[:c | prev _ prev isLetter ifTrue: [c asLowercase] ifFalse: [c asUppercase]]).
	^ true! !

!TextEditor methodsFor: 'editing keys' stamp: 'jmv 8/11/2010 10:08'!
makeLowercase: aKeyboardEvent
	"Force the current selection to lowercase.  Triggered by Cmd-X."

	self replaceSelectionWith: (self selection string asLowercase).
	^ true! !

!TextEditor methodsFor: 'editing keys' stamp: 'jmv 8/11/2010 10:08'!
makeUppercase: aKeyboardEvent
	"Force the current selection to uppercase.  Triggered by Cmd-Y."

	self replaceSelectionWith: (self selection string asUppercase).
	^ true! !

!TextEditor methodsFor: 'mvc compatibility' stamp: 'jmv 8/11/2010 10:43'!
zapSelectionWith: replacement

	| start stop rep |
	self deselect.
	start _ self startIndex.
	stop _ self stopIndex.
	(replacement isEmpty and: [stop > start]) ifTrue: [
		"If deleting, then set emphasisHere from 1st character of the deletion"
		emphasisHere _ (paragraph text attributesAt: start) select: [:att | att mayBeExtended]].

	(start = stop and: [ replacement isEmpty ]) ifFalse: [
		rep _ (replacement is: #Text)
			ifTrue: [ replacement ]
			ifFalse: [ Text string: replacement attributes: emphasisHere ].
		paragraph replaceFrom: start to: stop - 1 with: rep.
		self markIndex: start; pointIndex: start + rep size.
		UndoInterval _ otherInterval _ self selectionInterval.
		self userHasEdited  " -- note text now dirty" ].

	morph possiblyChanged ! !

!TextEditor methodsFor: 'new selection' stamp: 'jmv 8/11/2010 10:06'!
correctFrom: start to: stop with: aString
	"Make a correction in the model that the user has authorised from somewhere else in the system (such as from the compilier).  The user's selection is not changed, only corrected."
	| wasShowing userSelection delta loc |
	aString = '#insert period' ifTrue:
		[loc _ start.
		[(loc _ loc-1)>0 and: [(paragraph text string at: loc) isSeparator]]
			whileTrue: [loc _ loc-1].
		^ self correctFrom: loc+1 to: loc with: '.'].
	(wasShowing _ selectionShowing) ifTrue: [ self reverseSelection ].
	userSelection _ self selectionInterval.

	self selectInvisiblyFrom: start to: stop.
	self replaceSelectionWith: aString.

	delta _ aString size - (stop - start + 1).
	self selectInvisiblyFrom:
		userSelection first + (userSelection first > start ifFalse: [ 0 ] ifTrue: [ delta ])
		to: userSelection last + (userSelection last > start ifFalse: [ 0 ] ifTrue: [ delta ]).
	wasShowing ifTrue: [ self reverseSelection ].
! !

!TextEditor methodsFor: 'typing support' stamp: 'jmv 8/11/2010 10:01'!
addString: aString
	"Think of a better name"

	self zapSelectionWith: aString! !

!TextEditor methodsFor: 'typing support' stamp: 'jmv 9/21/2009 08:58'!
setEmphasisHereFromTextForward: f

	| i t forward delta prevIsSeparator nextIsSeparator |
	i _ self pointIndex.
	t _ paragraph text.
	"Try to set emphasisHere correctly after whitespace.
	Most important after a cr, i.e. at the start of a new line"
	prevIsSeparator _  i > 1 and: [ (t at: i-1) isSeparator ].
	nextIsSeparator _ i <= t size and: [ (t at: i) isSeparator ].
	forward _ prevIsSeparator = nextIsSeparator
		ifTrue: [ f ]
		ifFalse: [ nextIsSeparator ].
	delta _ forward ifTrue: [ 1 ] ifFalse: [ 0 ].
	emphasisHere _ (t attributesAt: (i - delta max: 1))
					select: [:att | att mayBeExtended].
	morph possiblyChanged ! !


!SmalltalkEditor methodsFor: 'private' stamp: 'jmv 8/11/2010 09:58'!
completeSymbol: hintText lastOffering: selectorOrNil
	"Invoked by Ctrl-q when there is only a caret.
		Do selector-completion, i.e., try to replace the preceding identifier by a
		selector that begins with those characters & has as many keywords as possible.
	 	Leave two spaces after each colon (only one after the last) as space for
		arguments.  Put the caret after the space after the first keyword.  If the
		user types Ctrl-q again immediately, choose a different selector.
	 Undoer: #undoQuery:lastOffering:; Redoer: itself.
	If redoing, just redisplay the last offering, selector[OrNil]."

	| firstTime input prior caret newStart sym kwds outStream |
	firstTime _ self isRedoing
		ifTrue: [prior _ sym _ selectorOrNil. true]
		ifFalse: [hintText isNil].
	firstTime
		ifTrue: "Initial Ctrl-q (or redo)"					
			[caret _ self startIndex.
			self selectPrecedingIdentifier.
			input _ self selection]
		ifFalse: "Repeated Ctrl-q"
			[caret _ UndoInterval first + hintText size.
			self selectInvisiblyFrom: UndoInterval first to: UndoInterval last.
			input _ hintText.
			prior _ selectorOrNil].
	(input size ~= 0 and: [sym notNil or:
			[(sym _ Symbol thatStarts: input string skipping: prior) notNil]])
		ifTrue: "found something to offer"
			[newStart _ self startIndex.
			outStream _ WriteStream on: (String new: 2 * sym size).
			1 to: (kwds _ sym keywords) size do:
				[:i |
				outStream nextPutAll: (kwds at: i).
				i = 1 ifTrue: [caret _ newStart + outStream contents size + 1].
				outStream nextPutAll:
					(i < kwds size ifTrue: ['  '] ifFalse: [' '])].
			UndoSelection _ input.
			self deselect; zapSelectionWith: outStream contents.
			self undoer: #undoQuery:lastOffering: with: input with: sym]
		ifFalse: "no more matches"
			[firstTime ifFalse: "restore original text & set up for a redo"
				[UndoSelection _ self selection.
				self deselect; zapSelectionWith: input.
				self undoer: #completeSymbol:lastOffering: with: input with: prior.
				Undone _ true].
			morph flash].
	self selectAt: caret! !


!TextMorph methodsFor: 'accessing' stamp: 'jmv 8/10/2010 14:07'!
newContents: stringOrText 
	"Accept new text contents."
	| newText embeddedMorphs |
	"If my text is all the same font, use the font for my new contents"
	newText _ stringOrText isString ifTrue: [ | textSize |
		(text notNil
		  and: [ (textSize _ text size) > 0
		    and: [ (text runLengthFor: 1) = textSize ]]) ifTrue: [ | attribs |
			attribs _ text attributesAt: 1.
			Text initialStyle: text initialStyleOrNil string: stringOrText copy attributes: attribs.
		]
		ifFalse: [ Text fromString: stringOrText copy ]
	]
	ifFalse: [ stringOrText copy asText.	"should be veryDeepCopy?" ].

	(text = newText and: [ text runs = newText runs ]) ifTrue: [ ^ self ].
	text ifNotNil: [(embeddedMorphs _ text embeddedMorphs)
			ifNotNil: 
				[self removeAllMorphsIn: embeddedMorphs.
				embeddedMorphs do: [:m | m delete]]].

	text _ newText.

	"add all morphs off the visible region; they'll be moved into the right 
	place when they become visible. (this can make the scrollable area too 
	large, though)"
	newText embeddedMorphs do: 
		[:m | 
		self addMorph: m.
		m position: -1000 @ 0].
	self releaseParagraph.
	"update the paragraph cache"
	self paragraph.
	"re-instantiate to set bounds"
	self world ifNotNil: [self world startSteppingSubmorphsOf: self]! !

!TextMorph methodsFor: 'private' stamp: 'jmv 8/10/2010 14:56'!
updateFromParagraph
	"A change has taken place in my paragraph, as a result of editing and I must be updated.  If a line break causes recomposition of the current paragraph, or it the selection has entered a different paragraph, then the current editor will be released, and must be reinstalled with the resulting new paragraph, while retaining any editor state, such as selection, undo state, and current typing emphasis."
	"removed multiple lined paragraph support (predecessor and successor)"

	| newStyle sel oldEditor |
	paragraph ifNil: [ ^self ].
	wrapFlag ifNil: [ wrapFlag := true ].
	editor ifNotNil: [
		oldEditor := editor.
		sel := editor selectionInterval.
		editor storeSelectionInParagraph].
	text := paragraph text.
	paragraph text initialStyle = text initialStyle 
		ifTrue: [self fit]
		ifFalse: [
			newStyle := paragraph text initialStyleOrNil.
			text initialStyle: newStyle.
			self
				releaseParagraph;					"Force recomposition"
				fit.									"and propagate the change"
			editor ifNotNil: [self installEditorToReplace: editor]].
	self layoutChanged.

	sel ifNotNil: [
		editor ifNil: [
			"Reinstate selection after, eg, style change"
			self installEditorToReplace: oldEditor]]! !

TextMorph removeSelector: #insertCharacters:!
Text removeSelector: #extendibleAttributesAt:!
