Skip to content

Commit ab938e8

Browse files
authored
Some CB fixes (#8507)
Partially addresses #8495 * Delete key not working in CB and in Code Editor * Cursor not placed at the end when adding new node with source node * Garbage added when edited node while having another one selected. * Closing CB when navigating. * Too eager selecting component after filtering update. * Premature node creation when dropping edges * Discarding changes when clicking-off CB
1 parent 857e874 commit ab938e8

File tree

7 files changed

+157
-129
lines changed

7 files changed

+157
-129
lines changed

app/gui2/src/components/CodeEditor.vue

+1
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ const editorStyle = computed(() => {
202202
:style="editorStyle"
203203
@keydown.enter.stop
204204
@keydown.backspace.stop
205+
@keydown.delete.stop
205206
@wheel.stop.passive
206207
@pointerdown.stop
207208
@contextmenu.stop

app/gui2/src/components/ComponentBrowser.vue

+28-47
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,15 @@ import type { Opt } from '@/util/opt'
1919
import { allRanges } from '@/util/range'
2020
import { Vec2 } from '@/util/vec2'
2121
import type { SuggestionId } from 'shared/languageServerTypes/suggestions'
22-
import type { ContentRange, ExprId } from 'shared/yjsModel.ts'
2322
import { computed, nextTick, onMounted, ref, watch, type ComputedRef, type Ref } from 'vue'
24-
import { useComponentBrowserInput } from './ComponentBrowser/input'
23+
import { useComponentBrowserInput, type Usage } from './ComponentBrowser/input'
2524
import GraphVisualization from './GraphEditor/GraphVisualization.vue'
2625
2726
const ITEM_SIZE = 32
2827
const TOP_BAR_HEIGHT = 32
2928
// Difference in position between the component browser and a node for the input of the component browser to
3029
// be placed at the same position as the node.
31-
const COMPONENT_BROWSER_TO_NODE_OFFSET = new Vec2(20, 35)
30+
const COMPONENT_BROWSER_TO_NODE_OFFSET = new Vec2(-4, -4)
3231
3332
const projectStore = useProjectStore()
3433
const suggestionDbStore = useSuggestionDbStore()
@@ -37,42 +36,24 @@ const graphStore = useGraphStore()
3736
const props = defineProps<{
3837
nodePosition: Vec2
3938
navigator: ReturnType<typeof useNavigator>
40-
initialContent: string
41-
initialCaretPosition: ContentRange
42-
sourcePort: Opt<ExprId>
39+
usage: Usage
4340
}>()
4441
4542
const emit = defineEmits<{
4643
accepted: [searcherExpression: string, requiredImports: RequiredImport[]]
47-
closed: [searcherExpression: string]
44+
closed: [searcherExpression: string, requiredImports: RequiredImport[]]
4845
canceled: []
4946
}>()
5047
51-
function getInitialContent(): string {
52-
if (props.sourcePort == null) return props.initialContent
53-
const sourceNodeName = graphStore.db.getOutputPortIdentifier(props.sourcePort)
54-
const sourceNodeNameWithDot = sourceNodeName ? sourceNodeName + '.' : ''
55-
return sourceNodeNameWithDot + props.initialContent
56-
}
57-
58-
function getInitialCaret(): ContentRange {
59-
if (props.sourcePort == null) return props.initialCaretPosition
60-
const sourceNodeName = graphStore.db.getOutputPortIdentifier(props.sourcePort)
61-
const sourceNodeNameWithDot = sourceNodeName ? sourceNodeName + '.' : ''
62-
return [
63-
props.initialCaretPosition[0] + sourceNodeNameWithDot.length,
64-
props.initialCaretPosition[1] + sourceNodeNameWithDot.length,
65-
]
66-
}
67-
6848
onMounted(() => {
6949
nextTick(() => {
70-
input.code.value = getInitialContent()
71-
const caret = getInitialCaret()
50+
input.reset(props.usage)
7251
if (inputField.value != null) {
7352
inputField.value.focus({ preventScroll: true })
74-
input.selection.value = { start: caret[0], end: caret[1] }
75-
selectLastAfterRefresh()
53+
} else {
54+
console.warn(
55+
'Component Browser input element was not mounted. This is not expected and may break the Component Browser',
56+
)
7657
}
7758
})
7859
})
@@ -108,7 +89,15 @@ const currentFiltering = computed(() => {
10889
)
10990
})
11091
111-
watch(currentFiltering, selectLastAfterRefresh)
92+
watch(currentFiltering, () => {
93+
selected.value = input.autoSelectFirstComponent.value ? 0 : null
94+
nextTick(() => {
95+
scrollToBottom()
96+
animatedScrollPosition.skip()
97+
animatedHighlightPosition.skip()
98+
animatedHighlightHeight.skip()
99+
})
100+
})
112101
113102
function readInputFieldSelection() {
114103
if (
@@ -159,9 +148,10 @@ useEvent(
159148
window,
160149
'pointerdown',
161150
(event) => {
151+
if (event.button !== 0) return
162152
if (!(event.target instanceof Element)) return
163153
if (!cbRoot.value?.contains(event.target)) {
164-
emit('closed', input.code.value)
154+
emit('closed', input.code.value, input.importsToAdd())
165155
}
166156
},
167157
{ capture: true },
@@ -263,21 +253,6 @@ const highlightClipPath = computed(() => {
263253
return `inset(${top}px 0px ${bottom}px 0px round 16px)`
264254
})
265255
266-
/**
267-
* Select the last element after updating component list.
268-
*
269-
* As the list changes the scroller's content, we need to wait a frame so the scroller
270-
* recalculates its height and setting scrollTop will work properly.
271-
*/
272-
function selectLastAfterRefresh() {
273-
selected.value = 0
274-
nextTick(() => {
275-
scrollToSelected()
276-
animatedScrollPosition.skip()
277-
animatedHighlightPosition.skip()
278-
})
279-
}
280-
281256
// === Scrolling ===
282257
283258
const scroller = ref<HTMLElement>()
@@ -297,6 +272,10 @@ function scrollToSelected() {
297272
scrollPosition.value = Math.max(selectedPosition.value - scrollerSize.value.y + ITEM_SIZE, 0)
298273
}
299274
275+
function scrollToBottom() {
276+
scrollPosition.value = listContentHeight.value - scrollerSize.value.y
277+
}
278+
300279
function updateScroll() {
301280
if (scroller.value && Math.abs(scroller.value.scrollTop - animatedScrollPosition.value) > 1.0) {
302281
scrollPosition.value = scroller.value.scrollTop
@@ -385,6 +364,9 @@ const handler = componentBrowserBindings.handler({
385364
@focusout="handleDefocus"
386365
@keydown="handler"
387366
@pointerdown.stop
367+
@keydown.enter.stop
368+
@keydown.backspace.stop
369+
@keydown.delete.stop
388370
>
389371
<div class="panels">
390372
<div class="panel components">
@@ -490,8 +472,7 @@ const handler = componentBrowserBindings.handler({
490472
v-model="input.code.value"
491473
name="cb-input"
492474
autocomplete="off"
493-
@keydown.backspace.stop
494-
@keyup="readInputFieldSelection"
475+
@input="readInputFieldSelection"
495476
/>
496477
</div>
497478
</div>

app/gui2/src/components/ComponentBrowser/input.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ import {
2121
type QualifiedName,
2222
} from '@/util/qualifiedName'
2323
import { equalFlat } from 'lib0/array'
24-
import { IdMap, type ContentRange } from 'shared/yjsModel'
24+
import { IdMap, type ContentRange, type ExprId } from 'shared/yjsModel'
2525
import { computed, ref, type ComputedRef } from 'vue'
2626

27+
/** Information how the component browser is used, needed for proper input initializing. */
28+
export type Usage =
29+
| { type: 'newNode'; sourcePort?: ExprId | undefined }
30+
| { type: 'editNode'; node: ExprId; cursorPos: number }
31+
2732
/** Input's editing context.
2833
*
2934
* It suggests what part of the input should be altered when accepting suggestion.
@@ -66,6 +71,7 @@ export function useComponentBrowserInput(
6671
const code = ref('')
6772
const selection = ref({ start: 0, end: 0 })
6873
const ast = computed(() => RawAstExtended.parse(code.value))
74+
const imports = ref<RequiredImport[]>([])
6975

7076
const context: ComputedRef<EditingContext> = computed(() => {
7177
const cursorPosition = selection.value.start
@@ -143,7 +149,20 @@ export function useComponentBrowserInput(
143149
return filter
144150
})
145151

146-
const imports = ref<RequiredImport[]>([])
152+
const autoSelectFirstComponent = computed(() => {
153+
// We want to autoselect first component only when we may safely assume user want's to continue
154+
// editing - they want to immediately see preview of best component and rather won't press
155+
// enter (and if press, they won't be surprised by the results).
156+
const ctx = context.value
157+
// If no input, we're sure user want's to add something.
158+
if (!code.value) return true
159+
// When changing identifier, it is unfinished. Or, the best match should be exactly what
160+
// the user wants
161+
if (ctx.type === 'changeIdentifier') return true
162+
// With partially written `.` chain we ssume user want's to add something.
163+
if (ctx.type === 'insert' && ctx.oprApp?.lastOpr()?.repr() === '.') return true
164+
return false
165+
})
147166

148167
function readOprApp(
149168
leafParent: IteratorResult<RawAstExtended<RawAst.Tree, false>>,
@@ -390,6 +409,27 @@ export function useComponentBrowserInput(
390409
}
391410
}
392411

412+
function reset(usage: Usage) {
413+
switch (usage.type) {
414+
case 'newNode':
415+
if (usage.sourcePort) {
416+
const sourceNodeName = graphDb.getOutputPortIdentifier(usage.sourcePort)
417+
code.value = sourceNodeName ? sourceNodeName + '.' : ''
418+
const caretPosition = code.value.length
419+
selection.value = { start: caretPosition, end: caretPosition }
420+
} else {
421+
code.value = ''
422+
selection.value = { start: 0, end: 0 }
423+
}
424+
break
425+
case 'editNode':
426+
code.value = graphDb.nodeIdToNode.get(usage.node)?.rootSpan.repr() ?? ''
427+
selection.value = { start: usage.cursorPos, end: usage.cursorPos }
428+
break
429+
}
430+
imports.value = []
431+
}
432+
393433
return {
394434
/** The current input's text (code). */
395435
code,
@@ -399,6 +439,10 @@ export function useComponentBrowserInput(
399439
context,
400440
/** The filter deduced from code and selection. */
401441
filter,
442+
/** Flag indicating that we should autoselect first component after last update */
443+
autoSelectFirstComponent,
444+
/** Re-initializes the input for given usage. */
445+
reset,
402446
/** Apply given suggested entry to the input. */
403447
applySuggestion,
404448
/** Return input after applying given suggestion, without changing state. */

0 commit comments

Comments
 (0)