Skip to content

Commit

Permalink
Stop using monaco content-widget for In-editor overlay optimization
Browse files Browse the repository at this point in the history
  • Loading branch information
nighca committed Jan 3, 2025
1 parent e8196f7 commit 4f7efaf
Show file tree
Hide file tree
Showing 21 changed files with 411 additions and 287 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
<script setup lang="ts">
function handleWheel(e: WheelEvent) {
// Prevent monaco editor from handling wheel event in completion card, see details in https://github.com/microsoft/monaco-editor/issues/2304
e.stopPropagation()
}
</script>

<template>
<div class="code-editor-card" @wheel="handleWheel">
<div class="code-editor-card">
<slot></slot>
</div>
</template>
Expand Down
11 changes: 0 additions & 11 deletions spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ export function useCodeEditorUICtx() {
if (ctx == null) throw new Error('useCodeEditorUICtx should be called inside of CodeEditorUI')
return ctx
}
export function makeContentWidgetEl() {
const el = document.createElement('div')
el.className = 'code-editor-content-widget'
return el
}
</script>

<script setup lang="ts">
Expand Down Expand Up @@ -356,11 +350,6 @@ function zoomReset() {
margin: 12px 0;
}
:global(.code-editor-content-widget) {
z-index: 10; // Ensure content widget is above other elements, especially cursor
padding: 2px 0; // Gap between content widget and text
}
.right-sidebar {
padding: 12px 8px;
flex: 0 0 auto;
Expand Down
22 changes: 21 additions & 1 deletion spx-gui/src/components/editor/code-editor/ui/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Sprite } from '@/models/sprite'
import { Sound } from '@/models/sound'
import { isWidget } from '@/models/widget'
import { type Range, type Position, type TextDocumentIdentifier, type Selection } from '../common'
import type { Monaco } from '../monaco'
import type { Monaco, MonacoEditor } from '../monaco'

export function token2Signal(token: monaco.CancellationToken): AbortSignal {
const ctrl = new AbortController()
Expand Down Expand Up @@ -77,3 +77,23 @@ export function supportGoTo(resourceModel: ResourceModel): boolean {
// Related issue: https://github.com/goplus/builder/issues/1139
return resourceModel instanceof Sprite || resourceModel instanceof Sound || isWidget(resourceModel)
}

/** Position in pixels relative to the viewport's top-left corner */
export type AbsolutePosition = {
top: number
left: number
height: number
}

export function toAbsolutePosition(position: Position, editor: MonacoEditor): AbsolutePosition | null {
const mPos = toMonacoPosition(position)
const editorPos = editor.getDomNode()?.getBoundingClientRect()
if (editorPos == null) return null
const scrolledVisiblePos = editor.getScrolledVisiblePosition(mPos)
if (scrolledVisiblePos == null) return null
return {
top: editorPos.top + scrolledVisiblePos.top,
left: editorPos.left + scrolledVisiblePos.left,
height: scrolledVisiblePos.height
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ function applyItem(item: InternalCompletionItem) {

<style lang="scss" scoped>
.completion-card {
width: 360px;
max-height: 200px;
width: 447px;
max-height: 227px;
padding: 12px 0 12px 12px;
display: flex;
align-items: stretch;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { UIDropdown, type DropdownPos } from '@/components/ui'
import { toAbsolutePosition } from '../common'
import { useCodeEditorUICtx } from '../CodeEditorUI.vue'
import type { CompletionController } from '.'
import CompletionCard from './CompletionCard.vue'
defineProps<{
const props = defineProps<{
controller: CompletionController
}>()
const codeEditorUICtx = useCodeEditorUICtx()
const dropdownVisible = ref(false)
const dropdownPos = ref<DropdownPos>({ x: 0, y: 0 })
watchEffect(() => {
const { currentCompletion, nonEmptyItems } = props.controller
if (currentCompletion == null || nonEmptyItems == null) {
dropdownVisible.value = false
return
}
const aPos = toAbsolutePosition(currentCompletion.position, codeEditorUICtx.ui.editor)
if (aPos == null) {
dropdownVisible.value = false
return
}
dropdownVisible.value = true
dropdownPos.value = {
x: aPos.left,
y: aPos.top,
width: 0,
height: aPos.height
}
})
</script>

<template>
<Teleport :to="controller.widgetEl">
<UIDropdown
:visible="dropdownVisible"
trigger="manual"
:pos="dropdownPos"
placement="bottom-start"
:offset="{ x: 0, y: 4 }"
>
<CompletionCard
v-if="controller.nonEmptyItems != null"
:items="controller.nonEmptyItems"
:controller="controller"
/>
</Teleport>
</UIDropdown>
</template>

<style lang="scss" scoped></style>
30 changes: 0 additions & 30 deletions spx-gui/src/components/editor/code-editor/ui/completion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
} from '../../common'
import { type monaco } from '../../monaco'
import type { CodeEditorUI } from '../code-editor-ui'
import { makeContentWidgetEl } from '../CodeEditorUI.vue'
import { fuzzyScoreGracefulAggressive as fuzzyScore, type FuzzyScore } from './fuzzy'

export type CompletionContext = BaseContext
Expand Down Expand Up @@ -53,25 +52,6 @@ export class CompletionController extends Emitter<{
super()
}

widgetEl = makeContentWidgetEl()

private widget = {
getId: () => `completion-for-${this.ui.id}`,
getDomNode: () => this.widgetEl,
getPosition: () => {
if (this.nonEmptyItems == null) return null
const monaco = this.ui.monaco
const cursorPos = this.ui.editor.getPosition()
return {
position: cursorPos,
preference: [
monaco.editor.ContentWidgetPositionPreference.BELOW,
monaco.editor.ContentWidgetPositionPreference.ABOVE
]
}
}
} satisfies monaco.editor.IContentWidget

private currentCompletionRef = shallowRef<{
ctrl: AbortController
textDocument: ITextDocument
Expand Down Expand Up @@ -223,15 +203,6 @@ export class CompletionController extends Emitter<{
})
)

// Manage completion widget (visibility, position, ...)
editor.addContentWidget(this.widget)
this.addDisposer(() => editor.removeContentWidget(this.widget))
this.addDisposer(
watch(
() => this.nonEmptyItems,
() => editor.layoutContentWidget(this.widget)
)
)
this.addDisposer(
watch(
() => [this.ui.activeTextDocument, this.ui.cursorPosition] as const,
Expand All @@ -242,7 +213,6 @@ export class CompletionController extends Emitter<{
!positionEq(this.currentCompletion.position, position)
) {
this.stopCompletion()
editor.layoutContentWidget(this.widget)
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import type { ContextMenuController, InternalMenuItem, MenuData } from '.'
const props = defineProps<{
controller: ContextMenuController
data: MenuData
data: MenuData | null
}>()
const pos = computed(() => {
if (props.data == null) return { x: 0, y: 0 }
return {
x: props.data.position.left,
y: props.data.position.top
Expand All @@ -21,9 +22,9 @@ function handleItemClick(item: InternalMenuItem) {
</script>

<template>
<UIDropdown class="context-menu" trigger="manual" visible :pos="pos" placement="bottom-start">
<UIDropdown class="context-menu" trigger="manual" :visible="data != null" :pos="pos" placement="bottom-start">
<UIMenu>
<UIMenuGroup v-for="(group, i) in data.groups" :key="i">
<UIMenuGroup v-for="(group, i) in data?.groups" :key="i">
<UIMenuItem v-for="(item, j) in group" :key="j" @click="handleItemClick(item)">
{{ item.title }}
</UIMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function handleClick(e: MouseEvent) {
display: flex;
padding: 0;
border: none;
outline: none;
background: var(--ui-color-grey-100);
cursor: pointer;
color: var(--ui-color-yellow-main);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defineProps<{
</script>

<template>
<ContextMenu v-if="controller.currentMenuData != null" :data="controller.currentMenuData" :controller="controller" />
<ContextMenu :data="controller.currentMenuData" :controller="controller" />
<Teleport :to="controller.menuTriggerWidgetEl">
<ContextMenuTrigger
v-if="controller.currentTriggerData != null"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
builtInCommandPaste,
type InternalAction
} from '../code-editor-ui'
import { makeContentWidgetEl } from '../CodeEditorUI.vue'
import { toMonacoPosition, fromMonacoPosition, fromMonacoSelection } from '../common'

export type ContextMenuContext = BaseContext
Expand Down Expand Up @@ -72,7 +71,7 @@ export class ContextMenuController extends Disposable {
return { selection: this.ui.selection! }
}

menuTriggerWidgetEl = makeContentWidgetEl()
menuTriggerWidgetEl = document.createElement('div')

private menuTriggerWidget = {
getId: () => `context-menu-trigger-for-${this.ui.id}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ async function handleAction(action: InternalAction) {
</script>

<template>
<CodeEditorCard class="hover-card">
<CodeEditorCard
class="hover-card"
@mouseenter="controller.emit('cardMouseEnter')"
@mouseleave="controller.emit('cardMouseLeave')"
>
<ul class="body">
<li v-for="(content, i) in hover.contents" :key="i" class="content">
<MarkdownView v-bind="content" />
Expand Down Expand Up @@ -64,7 +68,7 @@ async function handleAction(action: InternalAction) {
}
.content {
width: 320px;
width: 328px;
padding: 6px 8px;
}
Expand Down
41 changes: 38 additions & 3 deletions spx-gui/src/components/editor/code-editor/ui/hover/HoverUI.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { UIDropdown, type DropdownPos } from '@/components/ui'
import { toAbsolutePosition } from '../common'
import { useCodeEditorUICtx } from '../CodeEditorUI.vue'
import type { HoverController } from '.'
import HoverCard from './HoverCard.vue'
defineProps<{
const props = defineProps<{
controller: HoverController
}>()
const codeEditorUICtx = useCodeEditorUICtx()
const dropdownVisible = ref(false)
const dropdownPos = ref<DropdownPos>({ x: 0, y: 0 })
watchEffect(() => {
const hover = props.controller.currentHoverRef.value
if (hover == null) {
dropdownVisible.value = false
return
}
const aPos = toAbsolutePosition(hover.range.start, codeEditorUICtx.ui.editor)
if (aPos == null) {
dropdownVisible.value = false
return
}
dropdownVisible.value = true
dropdownPos.value = {
x: aPos.left,
y: aPos.top,
width: 0,
height: aPos.height
}
})
</script>

<template>
<Teleport :to="controller.widgetEl">
<UIDropdown
:visible="dropdownVisible"
trigger="manual"
:pos="dropdownPos"
placement="top-start"
:offset="{ x: 0, y: 4 }"
>
<HoverCard
v-if="controller.currentHoverRef.value != null"
:hover="controller.currentHoverRef.value"
:controller="controller"
/>
</Teleport>
</UIDropdown>
</template>

<style lang="scss" scoped></style>
Loading

0 comments on commit 4f7efaf

Please sign in to comment.