+
diff --git a/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue b/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue
index ddbe7d29d..f61d8d456 100644
--- a/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue
+++ b/spx-gui/src/components/editor/code-editor/ui/CodeEditorUI.vue
@@ -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
-}
-
+
-
+
diff --git a/spx-gui/src/components/editor/code-editor/ui/completion/index.ts b/spx-gui/src/components/editor/code-editor/ui/completion/index.ts
index dfd7b43e6..298435055 100644
--- a/spx-gui/src/components/editor/code-editor/ui/completion/index.ts
+++ b/spx-gui/src/components/editor/code-editor/ui/completion/index.ts
@@ -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
@@ -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
@@ -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,
@@ -242,7 +213,6 @@ export class CompletionController extends Emitter<{
!positionEq(this.currentCompletion.position, position)
) {
this.stopCompletion()
- editor.layoutContentWidget(this.widget)
}
}
)
diff --git a/spx-gui/src/components/editor/code-editor/ui/context-menu/ContextMenu.vue b/spx-gui/src/components/editor/code-editor/ui/context-menu/ContextMenu.vue
index 6db190327..43e3d65cc 100644
--- a/spx-gui/src/components/editor/code-editor/ui/context-menu/ContextMenu.vue
+++ b/spx-gui/src/components/editor/code-editor/ui/context-menu/ContextMenu.vue
@@ -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
@@ -21,9 +22,9 @@ function handleItemClick(item: InternalMenuItem) {
-
diff --git a/spx-gui/src/components/editor/code-editor/ui/hover/index.ts b/spx-gui/src/components/editor/code-editor/ui/hover/index.ts
index 4317d17d8..02c1ea0f5 100644
--- a/spx-gui/src/components/editor/code-editor/ui/hover/index.ts
+++ b/spx-gui/src/components/editor/code-editor/ui/hover/index.ts
@@ -1,6 +1,5 @@
-import { shallowRef, watch } from 'vue'
-import { debounce } from 'lodash'
-import { Disposable } from '@/utils/disposable'
+import { shallowRef } from 'vue'
+import Emitter from '@/utils/emitter'
import {
type Action,
type BaseContext,
@@ -12,6 +11,7 @@ import {
type ITextDocument,
containsPosition
} from '../../common'
+import type { monaco } from '../../monaco'
import {
builtInCommandCopilotFixProblem,
builtInCommandGoToResource,
@@ -20,9 +20,7 @@ import {
builtInCommandModifyResourceReference,
builtInCommandRenameResource
} from '../code-editor-ui'
-import { fromMonacoPosition, toMonacoPosition, token2Signal, supportGoTo } from '../common'
-import type { monaco } from '../../monaco'
-import { makeContentWidgetEl } from '../CodeEditorUI.vue'
+import { fromMonacoPosition, token2Signal, supportGoTo } from '../common'
export type Hover = {
contents: DefinitionDocumentationString[]
@@ -40,7 +38,10 @@ export interface IHoverProvider {
provideHover(ctx: HoverContext, position: Position): Promise
}
-export class HoverController extends Disposable {
+export class HoverController extends Emitter<{
+ cardMouseEnter: void
+ cardMouseLeave: void
+}> {
currentHoverRef = shallowRef(null)
private showHover(hover: InternalHover) {
@@ -61,22 +62,24 @@ export class HoverController extends Disposable {
super()
}
- widgetEl = makeContentWidgetEl()
+ /** `toHide` records the hover that is "planned" to be hiden, while with delay */
+ private toHide: InternalHover | null = null
- private widget: monaco.editor.IContentWidget = {
- getId: () => `hover-for-${this.ui.id}`,
- getDomNode: () => this.widgetEl,
- getPosition: () => {
- const monaco = this.ui.monaco
- const hover = this.currentHoverRef.value
- return {
- position: hover == null ? null : toMonacoPosition(hover.range.start),
- preference: [
- monaco.editor.ContentWidgetPositionPreference.ABOVE,
- monaco.editor.ContentWidgetPositionPreference.BELOW
- ]
+ /** Hide current hover with delay, to avoid flickering when mouse moves quickly */
+ private hideHoverWithDelay() {
+ const currentHover = this.currentHoverRef.value
+ if (currentHover == null) return
+ this.toHide = currentHover
+ setTimeout(() => {
+ if (this.currentHoverRef.value === this.toHide) {
+ this.hideHover()
}
- }
+ }, 150)
+ }
+
+ /** Cancel "planned" hiding */
+ private cancelHidingHover() {
+ this.toHide = null
}
private getDiagnosticsHover(textDocument: ITextDocument, position: monaco.Position): InternalHover | null {
@@ -146,8 +149,6 @@ export class HoverController extends Disposable {
this.addDisposable(
monaco.languages.registerHoverProvider('spx', {
provideHover: async (_, position, token) => {
- this.hideHover()
-
// TODO: use `onMouseMove` as trigger?
if (this.provider == null) return
const textDocument = this.ui.activeTextDocument
@@ -183,33 +184,34 @@ export class HoverController extends Disposable {
})
)
- this.addDisposer(
- watch(this.currentHoverRef, (hover, _, onCleanup) => {
- if (hover == null) return
- editor.addContentWidget(this.widget)
- onCleanup(() => editor.removeContentWidget(this.widget))
- })
- )
-
this.addDisposable(
- editor.onMouseMove(
- // debounce to avoid hiding when mouse moving from text to hover-widget, while through CONTENT_EMPTY
- debounce((e: monaco.editor.IEditorMouseEvent) => {
- if (
- e.target.type !== monaco.editor.MouseTargetType.CONTENT_WIDGET &&
- e.target.type !== monaco.editor.MouseTargetType.CONTENT_TEXT
- )
- this.hideHover()
- }, 100)
- )
+ editor.onMouseMove((e: monaco.editor.IEditorMouseEvent) => {
+ if (e.target.type !== monaco.editor.MouseTargetType.CONTENT_TEXT) {
+ this.hideHoverWithDelay()
+ return
+ }
+ const currentHover = this.currentHoverRef.value
+ const position = fromMonacoPosition(e.target.position)
+ if (currentHover != null && containsPosition(currentHover.range, position)) {
+ this.cancelHidingHover()
+ return
+ }
+ this.hideHoverWithDelay()
+ })
)
this.addDisposable(
- editor.onKeyDown(() => {
- this.hideHover()
+ editor.onMouseLeave(() => {
+ this.hideHoverWithDelay()
})
)
+ this.on('cardMouseEnter', () => this.cancelHidingHover())
+ this.on('cardMouseLeave', () => this.hideHoverWithDelay())
+
+ this.addDisposable(editor.onKeyDown(() => this.hideHover()))
+ this.addDisposable(editor.onMouseDown(() => this.hideHover()))
+
this.addDisposer(
resourceReferenceController.on('didStartModifying', () => {
this.hideHover()
diff --git a/spx-gui/src/components/editor/code-editor/ui/resource-reference/ResourceReferenceUI.vue b/spx-gui/src/components/editor/code-editor/ui/resource-reference/ResourceReferenceUI.vue
index 95962e23a..7e0be5c45 100644
--- a/spx-gui/src/components/editor/code-editor/ui/resource-reference/ResourceReferenceUI.vue
+++ b/spx-gui/src/components/editor/code-editor/ui/resource-reference/ResourceReferenceUI.vue
@@ -20,8 +20,10 @@ export function checkModifiable(el: HTMLElement): string | null {
-
+
controller.applySelected(newName)"
/>
-
+
-
-
diff --git a/spx-gui/src/components/ui/index.ts b/spx-gui/src/components/ui/index.ts
index 33575b626..95bda51cb 100644
--- a/spx-gui/src/components/ui/index.ts
+++ b/spx-gui/src/components/ui/index.ts
@@ -3,7 +3,7 @@ export { default as UICard } from './UICard.vue'
export { default as UICardHeader } from './UICardHeader.vue'
export { default as UIButton } from './UIButton.vue'
export { default as UIIconButton } from './UIIconButton.vue'
-export { default as UIDropdown } from './UIDropdown.vue'
+export { default as UIDropdown, type Pos as DropdownPos } from './UIDropdown'
export { default as UITooltip } from './UITooltip.vue'
export { UIMenu, UIMenuGroup, UIMenuItem } from './menu'
export { default as UIIcon, type Type as IconType } from './icons/UIIcon.vue'
diff --git a/spx-gui/src/components/ui/menu/UIMenuItem.vue b/spx-gui/src/components/ui/menu/UIMenuItem.vue
index dcb59dd11..201250dfa 100644
--- a/spx-gui/src/components/ui/menu/UIMenuItem.vue
+++ b/spx-gui/src/components/ui/menu/UIMenuItem.vue
@@ -13,7 +13,7 @@