1
0

Helios-Workspace.st 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. Smalltalk createPackage: 'Helios-Workspace'!
  2. Object subclass: #HLCodeModel
  3. instanceVariableNames: 'announcer environment receiver'
  4. package: 'Helios-Workspace'!
  5. !HLCodeModel methodsFor: 'accessing'!
  6. announcer
  7. ^ announcer ifNil: [ announcer := Announcer new ]
  8. !
  9. environment
  10. ^ environment ifNil: [ HLManager current environment ]
  11. !
  12. environment: anEnvironment
  13. environment := anEnvironment
  14. !
  15. receiver
  16. ^ receiver ifNil: [ receiver := self defaultReceiver ]
  17. !
  18. receiver: anObject
  19. receiver := anObject
  20. ! !
  21. !HLCodeModel methodsFor: 'actions'!
  22. doIt: aString
  23. ^ self environment eval: aString on: self receiver
  24. !
  25. inspect: anObject
  26. self environment inspect: anObject
  27. ! !
  28. !HLCodeModel methodsFor: 'defaults'!
  29. defaultReceiver
  30. ^ self environment doItReceiver
  31. ! !
  32. !HLCodeModel class methodsFor: 'actions'!
  33. on: anEnvironment
  34. ^ self new
  35. environment: anEnvironment;
  36. yourself
  37. ! !
  38. HLWidget subclass: #HLCodeWidget
  39. instanceVariableNames: 'model wrapper code editor state'
  40. package: 'Helios-Workspace'!
  41. !HLCodeWidget methodsFor: 'accessing'!
  42. announcer
  43. ^ self model announcer
  44. !
  45. contents
  46. ^ editor getValue
  47. !
  48. contents: aString
  49. editor setValue: aString.
  50. state ifNotNil: [ self updateState ]
  51. !
  52. currentLine
  53. ^editor getLine: (editor getCursor line)
  54. !
  55. currentLineOrSelection
  56. ^editor somethingSelected
  57. ifFalse: [ self currentLine ]
  58. ifTrue: [ self selection ]
  59. !
  60. editorOptions
  61. ^ #{
  62. 'theme' -> ('helios.codeMirrorTheme' settingValueIfAbsent: 'default helios').
  63. 'mode' -> 'text/x-stsrc'.
  64. 'lineNumbers' -> true.
  65. 'enterMode' -> 'flat'.
  66. 'indentWithTabs' -> true.
  67. 'indentUnit' -> 4.
  68. 'matchBrackets' -> true.
  69. 'electricChars' -> false.
  70. 'keyMap' -> 'Amber'.
  71. 'extraKeys' -> (HashedCollection with: ('helios.completionKey' settingValueIfAbsent: 'Shift-Space') -> 'autocomplete')
  72. }
  73. !
  74. model
  75. ^ model ifNil: [ model := HLCodeModel new ]
  76. !
  77. model: aModel
  78. model := aModel
  79. !
  80. receiver
  81. ^ self model receiver
  82. !
  83. receiver: anObject
  84. self model receiver: anObject
  85. !
  86. selection
  87. ^editor getSelection
  88. !
  89. selectionEnd
  90. ^code element selectionEnd
  91. !
  92. selectionEnd: anInteger
  93. code element selectionEnd: anInteger
  94. !
  95. selectionStart
  96. ^code element selectionStart
  97. !
  98. selectionStart: anInteger
  99. code element selectionStart: anInteger
  100. ! !
  101. !HLCodeWidget methodsFor: 'actions'!
  102. clear
  103. self contents: ''
  104. !
  105. configureEditor
  106. self editor at: 'amberCodeWidget' put: self.
  107. self editor on: 'change' do: [ self onChange ]
  108. !
  109. doIt
  110. | result |
  111. self model announcer announce: (HLDoItRequested on: model).
  112. result := model doIt: self currentLineOrSelection.
  113. self model announcer announce: (HLDoItExecuted on: model).
  114. ^ result
  115. !
  116. editor
  117. ^ editor
  118. !
  119. focus
  120. editor focus
  121. !
  122. inspectIt
  123. | newInspector |
  124. self model announcer announce: (HLInspectItRequested on: model).
  125. self model inspect: self doIt
  126. !
  127. print: aString
  128. | start stop currentLine |
  129. currentLine := (editor getCursor: false) line.
  130. start := HashedCollection new.
  131. start at: 'line' put: currentLine.
  132. start at: 'ch' put: (editor getCursor: false) ch.
  133. (editor getSelection) ifEmpty: [
  134. "select current line if selection is empty"
  135. start at: 'ch' put: (editor getLine: currentLine) size.
  136. editor setSelection: #{'line' -> currentLine. 'ch' -> 0} end: start.
  137. ].
  138. stop := HashedCollection new.
  139. stop at: 'line' put: currentLine.
  140. stop at: 'ch' put: ((start at: 'ch') + aString size + 2).
  141. editor replaceSelection: (editor getSelection, ' ', aString, ' ').
  142. editor setCursor: (editor getCursor: true).
  143. editor setSelection: stop end: start
  144. !
  145. printIt
  146. | result |
  147. result := self doIt.
  148. self model announcer announce: (HLPrintItRequested on: model).
  149. self print: result printString.
  150. self focus.
  151. !
  152. saveIt
  153. "I do not do anything"
  154. !
  155. setEditorOn: aTextarea
  156. <self['@editor'] = CodeMirror.fromTextArea(aTextarea, self._editorOptions())>
  157. ! !
  158. !HLCodeWidget methodsFor: 'hints'!
  159. messageHintFor: anEditor token: aToken
  160. ^ (Smalltalk vm allSelectors asArray
  161. select: [ :each | each includesSubString: aToken string ])
  162. reject: [ :each | each = aToken string ]
  163. !
  164. variableHintFor: anEditor token: aToken
  165. | variables classNames pseudoVariables |
  166. variables := (anEditor display wrapper asJQuery find: 'span.cm-variable') get
  167. collect: [ :each | each asJQuery html ].
  168. classNames := Smalltalk classes collect: [ :each | each name ].
  169. pseudoVariables := Smalltalk pseudoVariableNames.
  170. ^ ((variables, classNames, pseudoVariables) asSet asArray sort
  171. select: [ :each | each includesSubString: aToken string ])
  172. reject: [ :each | each = aToken string ]
  173. ! !
  174. !HLCodeWidget methodsFor: 'reactions'!
  175. onChange
  176. self updateState
  177. !
  178. onDoIt
  179. self doIt
  180. !
  181. onInspectIt
  182. self inspectIt
  183. !
  184. onPrintIt
  185. self printIt
  186. !
  187. onSaveIt
  188. "I do not do anything"
  189. ! !
  190. !HLCodeWidget methodsFor: 'rendering'!
  191. renderButtonsOn: html
  192. html button
  193. class: 'button';
  194. with: 'DoIt';
  195. onClick: [ self doIt ].
  196. html button
  197. class: 'button';
  198. with: 'PrintIt';
  199. onClick: [ self printIt ].
  200. html button
  201. class: 'button';
  202. with: 'InspectIt';
  203. onClick: [ self inspectIt ]
  204. !
  205. renderContentOn: html
  206. html div class: 'editor'; with: [
  207. code := html textarea ].
  208. state := html div class: 'state'.
  209. html div
  210. class: 'buttons_bar';
  211. with: [ self renderButtonsOn: html ].
  212. self
  213. setEditorOn: code element;
  214. configureEditor;
  215. updateState
  216. ! !
  217. !HLCodeWidget methodsFor: 'testing'!
  218. canHaveFocus
  219. ^ true
  220. !
  221. hasFocus
  222. ^ code asJQuery is: ':active'
  223. !
  224. hasModification
  225. ^ false
  226. ! !
  227. !HLCodeWidget methodsFor: 'updating'!
  228. updateState
  229. self hasModification
  230. ifTrue: [ state asJQuery addClass: 'modified' ]
  231. ifFalse: [ state asJQuery removeClass: 'modified' ]
  232. ! !
  233. !HLCodeWidget class methodsFor: 'accessing'!
  234. keyMap
  235. ^ HLManager current keyBinder systemIsMac
  236. ifTrue: [ self macKeyMap ]
  237. ifFalse: [ self pcKeyMap ]
  238. !
  239. macKeyMap
  240. ^ #{
  241. 'Alt-Backspace' -> 'delWordBefore'.
  242. 'Alt-Delete' -> 'delWordAfter'.
  243. 'Alt-Left' -> 'goWordLeft'.
  244. 'Alt-Right' -> 'goWordRight'.
  245. 'Cmd-A' -> 'selectAll'.
  246. 'Cmd-Alt-F' -> 'replace'.
  247. 'Cmd-D' -> 'doIt'.
  248. 'Cmd-Down' -> 'goDocEnd'.
  249. 'Cmd-End' -> 'goDocEnd'.
  250. 'Cmd-F' -> 'find'.
  251. 'Cmd-G' -> 'findNext'.
  252. 'Cmd-I' -> 'inspectIt'.
  253. 'Cmd-Left' -> 'goLineStart'.
  254. 'Cmd-P' -> 'printIt'.
  255. 'Cmd-Right' -> 'goLineEnd'.
  256. 'Cmd-S' -> 'saveIt'.
  257. 'Cmd-Up' -> 'goDocStart'.
  258. 'Cmd-Y' -> 'redo'.
  259. 'Cmd-Z' -> 'undo'.
  260. 'Cmd-[' -> 'indentLess'.
  261. 'Cmd-]' -> 'indentMore'.
  262. 'Ctrl-Alt-Backspace' -> 'delWordAfter'.
  263. 'Shift-Cmd-Alt-F' -> 'replaceAll'.
  264. 'Shift-Cmd-G' -> 'findPrev'.
  265. 'Shift-Cmd-Z' -> 'redo'.
  266. 'fallthrough' -> { 'basic'. 'emacsy' }
  267. }
  268. !
  269. pcKeyMap
  270. ^ #{
  271. 'Alt-Left' -> 'goLineStart'.
  272. 'Alt-Right' -> 'goLineEnd'.
  273. 'Alt-Up' -> 'goDocStart'.
  274. 'Ctrl-A' -> 'selectAll'.
  275. 'Ctrl-Backspace' -> 'delWordBefore'.
  276. 'Ctrl-D' -> 'doIt'.
  277. 'Ctrl-Delete' -> 'delWordAfter'.
  278. 'Ctrl-Down' -> 'goDocEnd'.
  279. 'Ctrl-End' -> 'goDocEnd'.
  280. 'Ctrl-F' -> 'find'.
  281. 'Ctrl-G' -> 'findNext'.
  282. 'Ctrl-I' -> 'inspectIt'.
  283. 'Ctrl-Home' -> 'goDocStart'.
  284. 'Ctrl-Left' -> 'goWordLeft'.
  285. 'Ctrl-P' -> 'printIt'.
  286. 'Ctrl-Right' -> 'goWordRight'.
  287. 'Ctrl-S' -> 'saveIt'.
  288. 'Ctrl-Y' -> 'redo'.
  289. 'Ctrl-Z' -> 'undo'.
  290. 'Ctrl-[' -> 'indentLess'.
  291. 'Ctrl-]' -> 'indentMore'.
  292. 'Shift-Ctrl-F' -> 'replace'.
  293. 'Shift-Ctrl-G' -> 'findPrev'.
  294. 'Shift-Ctrl-R' -> 'replaceAll'.
  295. 'Shift-Ctrl-Z' -> 'redo'.
  296. 'fallthrough' -> #('basic')
  297. }
  298. ! !
  299. !HLCodeWidget class methodsFor: 'hints'!
  300. hintFor: anEditor options: options
  301. | cursor token completions |
  302. cursor := anEditor getCursor.
  303. token := anEditor getTokenAt: cursor.
  304. token at: 'state' put: ((CodeMirror basicAt: 'innerMode')
  305. value: anEditor getMode value: (token at: 'state')) state.
  306. completions := token type = 'variable'
  307. ifTrue: [ HLCodeWidget variableHintFor: anEditor token: token ]
  308. ifFalse: [ HLCodeWidget messageHintFor: anEditor token: token ].
  309. ^ #{
  310. 'list' -> completions.
  311. 'from' -> ((CodeMirror basicAt: 'Pos') value: cursor line value: token end).
  312. 'to' -> ((CodeMirror basicAt: 'Pos') value: cursor line value: token start)
  313. }
  314. !
  315. messageHintFor: anEditor token: aToken
  316. ^ (anEditor at: 'amberCodeWidget')
  317. messageHintFor: anEditor token: aToken
  318. !
  319. variableHintFor: anEditor token: aToken
  320. ^ (anEditor at: 'amberCodeWidget')
  321. variableHintFor: anEditor token: aToken
  322. ! !
  323. !HLCodeWidget class methodsFor: 'initialization'!
  324. initialize
  325. super initialize.
  326. self
  327. setupCodeMirror;
  328. setupCommands;
  329. setupKeyMaps.
  330. !
  331. setupCodeMirror
  332. <
  333. CodeMirror.keyMap.default.fallthrough = ["basic"];
  334. CodeMirror.commands.autocomplete = function(cm) {
  335. CodeMirror.showHint(cm, self._hintFor_options_);
  336. }
  337. >
  338. !
  339. setupCommands
  340. (CodeMirror basicAt: 'commands')
  341. at: 'doIt' put: [ :cm | cm amberCodeWidget doIt ];
  342. at: 'inspectIt' put: [ :cm | cm amberCodeWidget inspectIt ];
  343. at: 'printIt' put: [ :cm | cm amberCodeWidget printIt ];
  344. at: 'saveIt' put: [ :cm | cm amberCodeWidget saveIt ]
  345. !
  346. setupKeyMaps
  347. <CodeMirror.keyMap['Amber'] = self._keyMap()>
  348. ! !
  349. HLCodeWidget subclass: #HLNavigationCodeWidget
  350. instanceVariableNames: 'methodContents'
  351. package: 'Helios-Workspace'!
  352. !HLNavigationCodeWidget methodsFor: 'accessing'!
  353. configureEditor
  354. super configureEditor.
  355. self contents: self methodContents
  356. !
  357. contents: aString
  358. self methodContents: aString.
  359. super contents: aString
  360. !
  361. methodContents
  362. ^ methodContents ifNil: [ '' ]
  363. !
  364. methodContents: aString
  365. ^ methodContents := aString
  366. !
  367. previous
  368. "for browser lists widget"
  369. !
  370. previous: aWidget
  371. "for browser lists widget"
  372. ! !
  373. !HLNavigationCodeWidget methodsFor: 'testing'!
  374. hasModification
  375. ^ (self methodContents = self contents) not
  376. ! !
  377. !HLNavigationCodeWidget class methodsFor: 'instance creation'!
  378. on: aBrowserModel
  379. ^ self new
  380. browserModel: aBrowserModel;
  381. yourself
  382. ! !
  383. !HLNavigationCodeWidget class methodsFor: 'testing'!
  384. canBeOpenAsTab
  385. ^ false
  386. ! !
  387. HLNavigationCodeWidget subclass: #HLBrowserCodeWidget
  388. instanceVariableNames: 'browserModel'
  389. package: 'Helios-Workspace'!
  390. !HLBrowserCodeWidget methodsFor: 'accessing'!
  391. browserModel
  392. ^ browserModel
  393. !
  394. browserModel: aBrowserModel
  395. browserModel := aBrowserModel.
  396. self
  397. observeSystem;
  398. observeBrowserModel
  399. ! !
  400. !HLBrowserCodeWidget methodsFor: 'actions'!
  401. observeBrowserModel
  402. self browserModel announcer
  403. on: HLSaveSourceCode
  404. send: #onSaveIt
  405. to: self;
  406. on: HLShowInstanceToggled
  407. send: #onShowInstanceToggled
  408. to: self;
  409. on: HLSourceCodeSaved
  410. send: #onSourceCodeSaved
  411. to: self;
  412. on: HLAboutToChange
  413. send: #onBrowserAboutToChange:
  414. to: self;
  415. on: HLParseErrorRaised
  416. send: #onParseError:
  417. to: self;
  418. on: HLCompileErrorRaised
  419. send: #onCompileError:
  420. to: self;
  421. on: HLUnknownVariableErrorRaised
  422. send: #onUnknownVariableError:
  423. to: self;
  424. on: HLInstVarAdded
  425. send: #onInstVarAdded
  426. to: self;
  427. on: HLMethodSelected
  428. send: #onMethodSelected:
  429. to: self;
  430. on: HLClassSelected
  431. send: #onClassSelected:
  432. to: self;
  433. on: HLPackageSelected
  434. send: #onPackageSelected:
  435. to: self;
  436. on: HLProtocolSelected
  437. send: #onProtocolSelected:
  438. to: self;
  439. on: HLSourceCodeFocusRequested
  440. send: #onSourceCodeFocusRequested
  441. to: self;
  442. on: HLShowTemplate
  443. send: #onShowTemplate:
  444. to: self
  445. !
  446. observeSystem
  447. self browserModel systemAnnouncer
  448. on: MethodModified
  449. send: #onMethodModified:
  450. to: self
  451. !
  452. refresh
  453. self hasModification ifTrue: [ ^ self ].
  454. self hasFocus ifTrue: [ ^ self ].
  455. self contents: self browserModel selectedMethod source
  456. !
  457. renderButtonsOn: html
  458. html button
  459. class: 'button';
  460. with: 'SaveIt';
  461. onClick: [ self saveIt ].
  462. super renderButtonsOn: html
  463. !
  464. saveIt
  465. self browserModel saveSourceCode
  466. !
  467. unregister
  468. super unregsiter.
  469. self browserModel announcer unsubscribe: self.
  470. self browserModel systemAnnouncer unsubscribe: self
  471. ! !
  472. !HLBrowserCodeWidget methodsFor: 'reactions'!
  473. onBrowserAboutToChange: anAnnouncement
  474. | block |
  475. block := anAnnouncement actionBlock.
  476. self hasModification
  477. ifTrue: [
  478. self
  479. confirm: 'Changes have not been saved. Do you want to discard these changes?'
  480. ifTrue: [
  481. "Don't ask twice"
  482. self methodContents: self contents.
  483. block value ].
  484. HLChangeForbidden signal ]
  485. !
  486. onClassSelected: anAnnouncement
  487. | class |
  488. class:= anAnnouncement item.
  489. class ifNil: [ ^ self contents: '' ].
  490. self contents: class definition
  491. !
  492. onCompileError: anAnnouncement
  493. self alert: anAnnouncement error messageText
  494. !
  495. onInstVarAdded
  496. self browserModel save: self contents
  497. !
  498. onMethodModified: anAnnouncement
  499. | method |
  500. method := anAnnouncement method.
  501. self browserModel selectedClass = method methodClass ifFalse: [ ^ self ].
  502. self browserModel selectedMethod ifNil: [ ^ self ].
  503. self browserModel selectedMethod selector = method selector ifFalse: [ ^ self ].
  504. self refresh
  505. !
  506. onMethodSelected: anAnnouncement
  507. | method |
  508. method := anAnnouncement item.
  509. method ifNil: [ ^ self contents: '' ].
  510. self contents: method source
  511. !
  512. onPackageSelected: anAnnouncement
  513. | package |
  514. package := anAnnouncement item.
  515. package ifNil: [ ^ self contents: '' ].
  516. self contents: package definition
  517. !
  518. onParseError: anAnnouncement
  519. | lineIndex newContents |
  520. lineIndex := 1.
  521. self contents: (String streamContents: [ :stream |
  522. self contents linesDo: [ :each |
  523. lineIndex = anAnnouncement line
  524. ifTrue: [
  525. stream
  526. nextPutAll: (each copyFrom: 1 to: anAnnouncement column);
  527. nextPutAll: '<- ';
  528. nextPutAll: anAnnouncement message;
  529. nextPutAll: ' ';
  530. nextPutAll: (each copyFrom: anAnnouncement column + 1 to: each size) ]
  531. ifFalse: [ stream nextPutAll: each ].
  532. stream nextPutAll: String cr.
  533. lineIndex := lineIndex + 1 ] ])
  534. !
  535. onProtocolSelected: anAnnouncement
  536. self browserModel selectedClass ifNil: [ ^ self contents: '' ].
  537. self contents: self browserModel selectedClass definition
  538. !
  539. onSaveIt
  540. self browserModel save: self contents
  541. !
  542. onShowInstanceToggled
  543. self browserModel selectedClass ifNil: [ ^ self contents: '' ].
  544. self contents: self browserModel selectedClass definition
  545. !
  546. onShowTemplate: anAnnouncement
  547. self contents: anAnnouncement template
  548. !
  549. onSourceCodeFocusRequested
  550. self focus
  551. !
  552. onSourceCodeSaved
  553. self methodContents: self contents.
  554. self updateState
  555. !
  556. onUnknownVariableError: anAnnouncement
  557. | error |
  558. error := anAnnouncement error.
  559. self
  560. confirm: (String streamContents: [ :stream |
  561. stream
  562. nextPutAll: error messageText;
  563. nextPutAll: String cr;
  564. nextPutAll: 'Would you like to define an instance variable?' ])
  565. ifTrue: [
  566. self browserModel addInstVarNamed: error variableName ]
  567. ! !
  568. !HLBrowserCodeWidget class methodsFor: 'instance creation'!
  569. on: aBrowserModel
  570. ^ self new
  571. browserModel: aBrowserModel;
  572. yourself
  573. ! !
  574. !HLBrowserCodeWidget class methodsFor: 'testing'!
  575. canBeOpenAsTab
  576. ^ false
  577. ! !
  578. HLWidget subclass: #HLWorkspace
  579. instanceVariableNames: 'codeWidget transcript'
  580. package: 'Helios-Workspace'!
  581. !HLWorkspace methodsFor: 'accessing'!
  582. codeWidget
  583. ^ codeWidget ifNil: [ codeWidget := HLCodeWidget new ]
  584. !
  585. transcript
  586. ^ transcript ifNil: [ transcript := HLTranscript new ]
  587. ! !
  588. !HLWorkspace methodsFor: 'actions'!
  589. focus
  590. ^ self codeWidget focus
  591. !
  592. unregister
  593. super unregister.
  594. self transcript unregister
  595. ! !
  596. !HLWorkspace methodsFor: 'rendering'!
  597. renderContentOn: html
  598. html with: (HLContainer with: (HLHorizontalSplitter
  599. with: self codeWidget
  600. with: [ :canvas | self renderTranscriptOn: canvas ]))
  601. !
  602. renderTranscriptOn: html
  603. html div
  604. class: 'transcript-container';
  605. with: [
  606. html div
  607. class: 'list-label';
  608. with: 'Transcript'.
  609. self transcript renderOn: html ]
  610. ! !
  611. !HLWorkspace methodsFor: 'testing'!
  612. canHaveFocus
  613. ^ true
  614. ! !
  615. !HLWorkspace class methodsFor: 'accessing'!
  616. tabClass
  617. ^ 'workspace'
  618. !
  619. tabLabel
  620. ^ 'Workspace'
  621. !
  622. tabPriority
  623. ^ 10
  624. ! !
  625. !HLWorkspace class methodsFor: 'testing'!
  626. canBeOpenAsTab
  627. ^ true
  628. ! !