From e585380434b142238fbd981ce876ab226e73958d Mon Sep 17 00:00:00 2001 From: nighca Date: Tue, 16 Sep 2025 18:33:49 +0800 Subject: [PATCH] Fix random inconsistent hover behavior --- .../editor/code-editor/code-editor.ts | 4 ++++ .../src/components/editor/code-editor/common.ts | 17 +++++++++++++++-- .../editor/code-editor/ui/hover/index.ts | 14 +++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/spx-gui/src/components/editor/code-editor/code-editor.ts b/spx-gui/src/components/editor/code-editor/code-editor.ts index 8e6ef7a0f..acb07c87d 100644 --- a/spx-gui/src/components/editor/code-editor/code-editor.ts +++ b/spx-gui/src/components/editor/code-editor/code-editor.ts @@ -528,6 +528,10 @@ class HoverProvider implements IHoverProvider { } const lspHover = await this.lspClient.textDocumentHover({ signal: ctx.signal }, lspParams) if (lspHover == null) return null + // LS may return hover info for a range that doesn't include the position (See details in https://github.com/goplus/builder/issues/2043). + // That may introduce inconsistency when there are also other hover UI sources (e.g., diagnostics, resource-references). + // So we drop such hover info here. + if (lspHover.range != null && !containsPosition(fromLSPRange(lspHover.range), position)) return null const contents: DefinitionDocumentationString[] = [] if (lsp.MarkupContent.is(lspHover.contents)) { // For now, we support MarkupContent only diff --git a/spx-gui/src/components/editor/code-editor/common.ts b/spx-gui/src/components/editor/code-editor/common.ts index a5c6b7e4e..cd49f09de 100644 --- a/spx-gui/src/components/editor/code-editor/common.ts +++ b/spx-gui/src/components/editor/code-editor/common.ts @@ -13,6 +13,14 @@ import { Animation } from '@/models/animation' import { isWidget } from '@/models/widget' import { stageCodeFilePaths } from '@/models/stage' +/** + * Position stands for the position of a **character** in the document. + * As LSP / Monaco position is "between two characters like an ‘insert’ cursor in an editor", + * we use the "cursor" position before the character when converting between our Position and LSP / Monaco position. + * See details in + * - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#position + * - https://microsoft.github.io/monaco-editor/docs.html#interfaces/IPosition.html#column + */ export type Position = { /** The line number, starting from `1` */ line: number @@ -21,7 +29,9 @@ export type Position = { } export type Range = { + /** The range's start position, inclusive */ start: Position + /** The range's end position, exclusive */ end: Position } @@ -456,7 +466,7 @@ export function isSelectionEmpty(selection: Selection | null) { export function containsPosition(range: Range, position: Position) { if (position.line < range.start.line || position.line > range.end.line) return false if (position.line === range.start.line && position.column < range.start.column) return false - if (position.line === range.end.line && position.column > range.end.column) return false + if (position.line === range.end.line && position.column >= range.end.column) return false return true } @@ -830,7 +840,10 @@ export function rangeEq(a: Range | null, b: Range | null) { } export function rangeContains(a: Range, b: Range) { - return containsPosition(a, b.start) && containsPosition(a, b.end) + return ( + (positionEq(a.start, b.start) || positionAfter(b.start, a.start)) && + (positionEq(a.end, b.end) || positionAfter(a.end, b.end)) + ) } const textDocumentURIPrefix = 'file:///' 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 b48163671..12b4fd7e3 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 @@ -241,7 +241,19 @@ export class HoverController extends Emitter<{ handleMouseEnter({ type: 'other' }) return } - const position = fromMonacoPosition(e.target.position) + // Here we use start position of `e.target.range` instead of `e.target.position`, as hovering happens on one character instead of between two characters. + // And `e.target.position` stands for the gap between two characters while `e.target.range` represents the hovered character. + // For example with text `ab`: + // * When the mouse is over the second half of `a`: + // - `e.target.position` will be `{ column: 2 }` + // - `e.target.range` will be `{ startColumn: 1, endColumn: 2 }` + // * When the mouse is over the first half of `b`: + // - `e.target.position` will be `{ column: 2 }` + // - `e.target.range` will be `{ startColumn: 2, endColumn: 3 }` + const position = fromMonacoPosition({ + lineNumber: e.target.range.startLineNumber, + column: e.target.range.startColumn + }) handleMouseEnter({ type: 'text', position }) }) )