Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit 001236a

Browse files
committed
fix(plugins/plugin-client-common): a few small refinements to "editing" UI for Inputs
Fixes #5656 Fixes #5658
1 parent 4296933 commit 001236a

File tree

9 files changed

+71
-40
lines changed

9 files changed

+71
-40
lines changed

packages/core/src/webapp/cancel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Block } from './models/block'
2828
import { ExecType } from '../models/command'
2929
import { CommandCompleteEvent } from '../repl/events'
3030

31-
export default function doCancel(tab: Tab, block: Block) {
31+
export default function doCancel(tab: Tab, block: Block, valueTypedSoFar: string) {
3232
block.isCancelled = true
3333

3434
const execUUID = block.getAttribute('data-uuid')
@@ -38,7 +38,7 @@ export default function doCancel(tab: Tab, block: Block) {
3838
cancelled: true,
3939
execUUID,
4040
historyIdx: -1,
41-
command: undefined,
41+
command: valueTypedSoFar,
4242
argvNoOptions: undefined,
4343
execOptions: undefined,
4444
parsedOptions: undefined,

plugins/plugin-bash-like/src/test/bash-like/ctrl-c.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ describe(`Cancel via Ctrl+C ${process.env.MOCHA_RUN_TARGET || ''}`, function(thi
4040
.catch(Common.oops(this, true))
4141

4242
it('should hit ctrl+c', () => cancel(this.app))
43+
it('should clear the terminal', () =>
44+
CLI.command('clear', this.app).then(() => ReplExpect.consoleToBeClear(this.app)))
4345
it('should type foo and hit ctrl+c', () => cancel(this.app, 'foo'))
4446

4547
it('should cancel a non-pty command via ctrl+c', async () => {

plugins/plugin-client-common/i18n/resources_en_US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"ExperimentalTag": "Experimental",
33
"HoverExperimentalTag": "This command is highly experimental, and may change radically in the near future. Proceed with caution.",
4+
"Cancel edit": "Currently in edit mode. Click to cancel.",
45
"Remove": "Remove",
56
"ok": "ok",
67
"Status Grid": "Status Grid",

plugins/plugin-client-common/src/components/Views/Terminal/Block/BlockModel.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export type AnnouncementBlock = WithState<BlockState.ValidResponse> &
5555
WithResponse<ScalarResponse> &
5656
WithCWD &
5757
WithAnnouncement
58-
type EmptyBlock = WithState<BlockState.Empty> & WithCWD
58+
type EmptyBlock = WithState<BlockState.Empty> & WithCWD & Partial<WithCommand>
5959
type ErrorBlock = WithState<BlockState.Error> &
6060
WithCommand &
6161
WithResponse<Error> &
@@ -129,7 +129,7 @@ export function isFinished(block: BlockModel): block is FinishedBlock {
129129
}
130130

131131
export function hasCommand(block: BlockModel & Partial<WithCommand>): block is BlockModel & Required<WithCommand> {
132-
return !isActive(block) && !isEmpty(block)
132+
return !isActive(block) && (!isEmpty(block) || block.command !== undefined)
133133
}
134134

135135
export function isAnnouncement(block: BlockModel): block is AnnouncementBlock {
@@ -185,15 +185,16 @@ export function Processing(
185185
}
186186

187187
/** Transform to Empty */
188-
export function Empty(block: BlockModel): EmptyBlock {
188+
export function Empty(block: BlockModel, typedSoFar?: string): EmptyBlock {
189189
return {
190190
cwd: block.cwd,
191+
command: typedSoFar,
191192
state: BlockState.Empty
192193
}
193194
}
194195

195196
/** Transform to Cancelled */
196-
export function Cancelled(block: BlockModel): CancelledBlock | EmptyBlock {
197+
export function Cancelled(block: BlockModel, typedSoFar?: string): CancelledBlock | EmptyBlock {
197198
if (isProcessing(block)) {
198199
return {
199200
cwd: block.cwd,
@@ -203,7 +204,7 @@ export function Cancelled(block: BlockModel): CancelledBlock | EmptyBlock {
203204
state: BlockState.Cancelled
204205
}
205206
} else {
206-
return Empty(block)
207+
return Empty(block, typedSoFar)
207208
}
208209
}
209210

plugins/plugin-client-common/src/components/Views/Terminal/Block/Input.tsx

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
import { BlockViewTraits, BlockOperationTraits } from './'
4242

4343
import Tag from '../../../spi/Tag'
44+
import Icons from '../../../spi/Icons'
4445
import ExpandableSection from '../../../spi/ExpandableSection'
4546

4647
const SimpleEditor = React.lazy(() => import('../../../Content/Editor/SimpleEditor'))
@@ -151,10 +152,26 @@ export abstract class InputProvider<S extends State = State> extends React.PureC
151152
// eslint-disable-next-line @typescript-eslint/no-empty-function
152153
protected status() {}
153154

155+
protected cancelReEdit() {
156+
this.setState(() => {
157+
return {
158+
isReEdit: false
159+
}
160+
})
161+
}
162+
163+
private readonly _cancelReEdit = this.cancelReEdit.bind(this)
164+
154165
protected contextContent(
155166
insideBrackets: React.ReactNode = this.props.displayedIdx || this.props.idx + 1
156167
): React.ReactNode {
157-
return <React.Fragment>In[{insideBrackets}]</React.Fragment> // this.props.model.cwd
168+
return this.state.isReEdit ? (
169+
<a href="#" className="kui--block-action" title={strings('Cancel edit')} onClick={this._cancelReEdit}>
170+
<Icons icon="Edit" className="clickable" />
171+
</a>
172+
) : (
173+
<React.Fragment>In[{insideBrackets}]</React.Fragment>
174+
) // this.props.model.cwd
158175
}
159176

160177
/** the "xxx" part of "xxx >" of the prompt */
@@ -291,11 +308,20 @@ export default class Input extends InputProvider {
291308
}
292309
}
293310

294-
/** @return the value of the prompt */
311+
/** @return the current value of the prompt */
295312
public value() {
296313
return this.state.prompt && this.state.prompt.value
297314
}
298315

316+
/** @return the value to be added to the prompt */
317+
protected valueToBeDisplayed() {
318+
return hasValue(this.props.model)
319+
? this.props.model.value
320+
: hasCommand(this.props.model)
321+
? this.props.model.command
322+
: ''
323+
}
324+
299325
/** Owner wants us to focus on the current prompt */
300326
public doFocus() {
301327
if (this.props.isFocused && this.state.prompt) {
@@ -417,7 +443,10 @@ export default class Input extends InputProvider {
417443
}
418444

419445
return (
420-
<div className="repl-input-element-wrapper flex-layout flex-fill">
446+
<div
447+
className="repl-input-element-wrapper flex-layout flex-fill"
448+
data-is-reedit={this.state.isReEdit || undefined}
449+
>
421450
<input
422451
type="text"
423452
autoFocus={this.props.isFocused && isInViewport(this.props._block)}
@@ -433,11 +462,14 @@ export default class Input extends InputProvider {
433462
this.props.onInputBlur && this.props.onInputBlur(evt)
434463

435464
const valueNotChanged =
436-
hasCommand(this.props.model) && this.props.model.command === this.state.prompt.value
465+
hasCommand(this.props.model) &&
466+
this.state.prompt &&
467+
this.props.model.command === this.state.prompt.value
437468
this.setState(curState => {
438469
if (curState.isReEdit && valueNotChanged) {
439470
return {
440-
isReEdit: false
471+
isReEdit: false,
472+
prompt: undefined
441473
}
442474
}
443475
})
@@ -457,17 +489,10 @@ export default class Input extends InputProvider {
457489
ref={this._onRef}
458490
/>
459491
{this.state.typeahead && <span className="kui--input-typeahead">{this.state.typeahead}</span>}
460-
{<span className="repl-prompt-right-elements">{this.reEditing()}</span>}
461492
</div>
462493
)
463494
} else {
464-
const value =
465-
this.value() ||
466-
(hasValue(this.props.model)
467-
? this.props.model.value
468-
: hasCommand(this.props.model)
469-
? this.props.model.command
470-
: '')
495+
const value = this.valueToBeDisplayed()
471496

472497
if (isProcessing(this.props.model)) {
473498
// for processing blocks, we still need an input, albeit
@@ -480,7 +505,7 @@ export default class Input extends InputProvider {
480505
value={value}
481506
onKeyDown={evt => {
482507
if (evt.key === 'c' && evt.ctrlKey) {
483-
doCancel(this.props.tab, this.props._block)
508+
doCancel(this.props.tab, this.props._block, value)
484509
}
485510
}}
486511
ref={c => c && c.focus()}
@@ -505,6 +530,7 @@ export default class Input extends InputProvider {
505530
}}
506531
>
507532
<span className="repl-input-element flex-fill">{value}</span>
533+
{value.length === 0 && <span className="kui--repl-input-element-nbsp">&nbsp;</span> /* &nbsp; */}
508534
{this.inputStatus(value)}
509535
</div>
510536
)
@@ -544,13 +570,6 @@ export default class Input extends InputProvider {
544570
}
545571
}
546572

547-
/** render an re-edit indicator when users clicked to re-edit an input */
548-
private reEditing() {
549-
if (this.state.isReEdit) {
550-
return <span className="kui--repl-block-re-editing kui--repl-block-right-element">{strings('editing...')}</span>
551-
}
552-
}
553-
554573
/** spinner for processing blocks */
555574
private spinner() {
556575
return (
@@ -572,7 +591,7 @@ export default class Input extends InputProvider {
572591

573592
/** DropDown menu for completed blocks */
574593
private actions(command: string) {
575-
if (isFinished(this.props.model) && this.props.tab && this.props.model) {
594+
if (isFinished(this.props.model) && !!this.props.tab && !!this.props.model) {
576595
return <Actions command={command} {...this.props} />
577596
}
578597
}

plugins/plugin-client-common/src/components/Views/Terminal/Block/OnKeyDown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default async function onKeyDown(this: Input, event: KeyboardEvent) {
112112
}
113113
} else if (char === KeyCodes.C && event.ctrlKey) {
114114
// Ctrl+C, cancel
115-
doCancel(tab, block) // eslint-disable-line @typescript-eslint/no-use-before-define
115+
doCancel(tab, block, prompt.value)
116116
} else if (char === KeyCodes.U && event.ctrlKey) {
117117
// clear line
118118
prompt.value = ''

plugins/plugin-client-common/src/components/Views/Terminal/ScrollableTerminal.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -421,19 +421,21 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
421421
} else if (curState.focusedBlockIdx !== undefined && curState.focusedBlockIdx !== idx) {
422422
// Transform the active block to Processing
423423
const activeBlockIdx = curState.focusedBlockIdx
424+
const blocks = curState.blocks
425+
.slice(0, activeBlockIdx)
426+
.concat([Processing(curState.blocks[activeBlockIdx], event, event.evaluatorOptions.isExperimental)])
427+
.concat(curState.blocks.slice(activeBlockIdx + 1))
424428

425429
return {
426-
blocks: curState.blocks
427-
.slice(0, activeBlockIdx)
428-
.concat([Processing(curState.blocks[activeBlockIdx], event, event.evaluatorOptions.isExperimental)])
429-
.concat(curState.blocks.slice(activeBlockIdx + 1))
430+
blocks
430431
}
431432
} else {
432433
// Transform the last block to Processing
434+
const blocks = curState.blocks
435+
.slice(0, idx)
436+
.concat([Processing(curState.blocks[idx], event, event.evaluatorOptions.isExperimental)])
433437
return {
434-
blocks: curState.blocks
435-
.slice(0, idx)
436-
.concat([Processing(curState.blocks[idx], event, event.evaluatorOptions.isExperimental)])
438+
blocks
437439
}
438440
}
439441
})
@@ -499,7 +501,7 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
499501
const inProcess = curState.blocks[inProcessIdx]
500502
const blocks = curState.blocks
501503
.slice(0, inProcessIdx)
502-
.concat([Cancelled(inProcess)]) // mark as cancelled
504+
.concat([Cancelled(inProcess, event.command)]) // mark as cancelled
503505
.concat(curState.blocks.slice(inProcessIdx + 1))
504506
.concat(inProcessIdx === curState.blocks.length - 1 ? [Active()] : []) // plus a new block if needed
505507
return {
@@ -958,7 +960,7 @@ export default class ScrollableTerminal extends React.PureComponent<Props, State
958960
.filter(_ => !isHidden(_))
959961
.map((_, idx) => (
960962
<Block
961-
key={(hasUUID(_) ? _.execUUID : idx) + `-${idx}-isPartOfMiniSplit=${isMiniSplit}`}
963+
key={(hasUUID(_) ? _.execUUID : _.state) + `-${idx}-isPartOfMiniSplit=${isMiniSplit}`}
962964
idx={idx}
963965
displayedIdx={findDisplayedIdx(idx)}
964966
model={_}

plugins/plugin-client-common/web/scss/components/Terminal/Block.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ $action-hover-delay: 140ms;
216216
align-items: stretch;
217217
padding-left: 0.5rem;
218218
position: relative; /* for repl-block-actions' position: absolute */
219-
.repl-input-element {
219+
.repl-input-element,
220+
.kui--repl-input-element-nbsp {
220221
padding: $inset 0;
221222
padding-right: $inset; /* to pad against timestamp and action buttons */
222223
}
@@ -226,6 +227,10 @@ $action-hover-delay: 140ms;
226227
.repl-input-element-wrapper {
227228
background-color: $input-bg;
228229
border-color: $input-border;
230+
231+
&[data-is-reedit] {
232+
border-color: $input-border-editing;
233+
}
229234
}
230235
}
231236
.repl-output .repl-context {

plugins/plugin-client-common/web/scss/components/Terminal/_mixins.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
$input-bg: var(--color-table-border3);
1818
$input-border: rgba(100, 100, 100, 0.05);
19+
$input-border-editing: var(--color-brand-03);
1920

2021
@mixin Scrollback {
2122
.kui--scrollback {

0 commit comments

Comments
 (0)