|
16 | 16 | import { derived } from "svelte/store" |
17 | 17 | import FieldIcon from "../blocks/field-icon/field-icon.svelte" |
18 | 18 | import { type Field } from "@undb/table" |
| 19 | + import { computePosition, flip, shift, offset } from "@floating-ui/dom" |
| 20 | + import { globalFormulaRegistry } from "@undb/formula/src/formula/formula.registry" |
19 | 21 |
|
20 | 22 | const functions = FORMULA_FUNCTIONS |
21 | 23 |
|
|
31 | 33 | export let value: string = "" |
32 | 34 |
|
33 | 35 | let editor: EditorView |
34 | | - let suggestions: string[] = [...functions, ...$fields] |
| 36 | + let formulaSuggestions: string[] = [...functions] |
| 37 | + let fieldSuggestions: string[] = [...$fields] |
| 38 | +
|
| 39 | + $: suggestions = [...formulaSuggestions, ...fieldSuggestions] |
| 40 | +
|
35 | 41 | let selectedSuggestion: string = "" |
| 42 | + let hoverSuggestion: string = "" |
| 43 | +
|
| 44 | + $: hoverFormula = hoverSuggestion ? globalFormulaRegistry.get(hoverSuggestion as FormulaFunction) : undefined |
36 | 45 |
|
37 | 46 | const highlightStyle = HighlightStyle.define([ |
38 | 47 | { tag: tags.keyword, color: "#5c6bc0" }, |
|
281 | 290 | if (!isInsideParens) { |
282 | 291 | const functionNode = visitor.getNearestFunctionNode() |
283 | 292 | if (functionNode) { |
284 | | - const functionStart = functionNode.start.startIndex |
285 | | - const functionNameLength = functionNode.IDENTIFIER().text.length |
| 293 | + const functionStart = functionNode.start.tokenIndex |
| 294 | + const functionNameLength = functionNode.IDENTIFIER().getText().length |
286 | 295 | const transaction = editor.state.update({ |
287 | 296 | changes: { |
288 | 297 | from: functionStart, |
|
311 | 320 | editor.dispatch(transaction) |
312 | 321 | } else { |
313 | 322 | const fieldWithBrackets = `{{${suggestion}}}` |
314 | | -
|
315 | | - // 使用正则表达式找到光标位置最近的完整变量 |
316 | 323 | const fullText = editor.state.doc.toString() |
317 | | - let start = cursor |
318 | | - let end = cursor |
| 324 | +
|
| 325 | + // 检查光标是否在变量内部 |
| 326 | + let isInsideVariable = false |
| 327 | + let variableStart = -1 |
| 328 | + let variableEnd = -1 |
319 | 329 |
|
320 | 330 | // 向前搜索 {{ |
321 | 331 | for (let i = cursor; i >= 0; i--) { |
322 | 332 | if (fullText.slice(i, i + 2) === "{{") { |
323 | | - start = i |
| 333 | + variableStart = i |
324 | 334 | break |
325 | 335 | } |
326 | 336 | } |
327 | 337 |
|
328 | 338 | // 向后搜索 }} |
329 | 339 | for (let i = cursor; i < fullText.length; i++) { |
330 | 340 | if (fullText.slice(i, i + 2) === "}}") { |
331 | | - end = i + 2 |
| 341 | + variableEnd = i + 2 |
332 | 342 | break |
333 | 343 | } |
334 | 344 | } |
335 | 345 |
|
336 | | - // 检查找到的范围是否是一个有效的变量(不超过最近的逗号) |
337 | | - const textBetween = fullText.slice(start, end) |
338 | | - const isValidVariable = textBetween.includes("{{") && textBetween.includes("}}") && !textBetween.includes(",") |
| 346 | + // 判断光标是否在变量内部 |
| 347 | + if (variableStart !== -1 && variableEnd !== -1) { |
| 348 | + const textBetween = fullText.slice(variableStart, variableEnd) |
| 349 | + isInsideVariable = textBetween.includes("{{") && textBetween.includes("}}") && !textBetween.includes(",") |
| 350 | + } |
339 | 351 |
|
340 | | - if (isValidVariable) { |
341 | | - // 替换找到的变量 |
| 352 | + // 如果光标在变量内部,替换变量 |
| 353 | + // 如果光标在变量后面或其他位置,直接在当前位置插入 |
| 354 | + if (isInsideVariable && cursor < variableEnd) { |
342 | 355 | const transaction = editor.state.update({ |
343 | 356 | changes: { |
344 | | - from: start, |
345 | | - to: end, |
| 357 | + from: variableStart, |
| 358 | + to: variableEnd, |
346 | 359 | insert: fieldWithBrackets, |
347 | 360 | }, |
348 | 361 | selection: { |
349 | | - anchor: start + fieldWithBrackets.length, |
| 362 | + anchor: variableStart + fieldWithBrackets.length, |
350 | 363 | }, |
351 | 364 | }) |
352 | 365 | editor.dispatch(transaction) |
353 | 366 | } else { |
354 | | - // 不在变量内部或范��无效,直接在当前位置插入新变量 |
355 | 367 | const transaction = editor.state.update({ |
356 | 368 | changes: { |
357 | 369 | from: cursor, |
|
380 | 392 | errorMessage = (error as Error).message |
381 | 393 | } |
382 | 394 | } |
| 395 | +
|
| 396 | + let hoverSuggestionContainer: HTMLElement |
| 397 | + let editorContainerWrapper: HTMLElement |
| 398 | +
|
| 399 | + function update() { |
| 400 | + if (hoverSuggestionContainer && editorContainerWrapper && hoverFormula) { |
| 401 | + computePosition(editorContainerWrapper, hoverSuggestionContainer, { |
| 402 | + placement: "left-start", |
| 403 | + middleware: [flip(), shift({ padding: 5 }), offset(10)], |
| 404 | + }).then(({ x, y }) => { |
| 405 | + Object.assign(hoverSuggestionContainer.style, { |
| 406 | + left: `${x}px`, |
| 407 | + top: `${y}px`, |
| 408 | + }) |
| 409 | + }) |
| 410 | + } |
| 411 | + } |
| 412 | +
|
| 413 | + onMount(() => { |
| 414 | + update() |
| 415 | + }) |
383 | 416 | </script> |
384 | 417 |
|
385 | | -<div> |
| 418 | +<div bind:this={editorContainerWrapper} id="editor-container-wrapper" class="relative"> |
386 | 419 | <div id="editor-container" class="mb-2 rounded-sm border"></div> |
387 | 420 | {#if errorMessage} |
388 | 421 | <p class="text-destructive flex items-center gap-1 text-xs"> |
|
391 | 424 | </p> |
392 | 425 | {/if} |
393 | 426 |
|
394 | | - <ul class="mt-2 flex h-[250px] flex-col overflow-auto rounded-lg border border-gray-200"> |
395 | | - {#each suggestions as suggestion} |
| 427 | + <ul class="mt-2 flex h-[250px] flex-col divide-y overflow-auto rounded-lg border border-gray-200"> |
| 428 | + <div class="sticky top-0 z-10 border-b bg-gray-100 px-2 py-1.5 text-xs font-semibold">Formula</div> |
| 429 | + {#each formulaSuggestions as suggestion} |
| 430 | + {@const isSelected = suggestion === selectedSuggestion} |
| 431 | + {@const isHovered = suggestion === hoverSuggestion} |
| 432 | + <button |
| 433 | + type="button" |
| 434 | + on:click={() => insertSuggestion(suggestion)} |
| 435 | + on:mouseenter={() => { |
| 436 | + hoverSuggestion = suggestion |
| 437 | + update() |
| 438 | + }} |
| 439 | + class="group relative w-full text-left text-xs font-medium" |
| 440 | + > |
| 441 | + <li |
| 442 | + class={cn("flex w-full items-center gap-1 p-2 hover:bg-gray-100", (isSelected || isHovered) && "bg-gray-100")} |
| 443 | + > |
| 444 | + <span class="font-normal"> |
| 445 | + <SquareFunctionIcon class="size-4" /> |
| 446 | + </span> |
| 447 | + <span> |
| 448 | + {suggestion}() |
| 449 | + </span> |
| 450 | + </li> |
| 451 | + |
| 452 | + <div class="absolute left-0 top-0 z-50 -translate-x-[100%] group-hover:block">hello</div> |
| 453 | + </button> |
| 454 | + {/each} |
| 455 | + <div class="sticky top-0 z-10 border-b bg-gray-100 px-2 py-1.5 text-xs font-semibold">Field</div> |
| 456 | + {#each fieldSuggestions as suggestion} |
396 | 457 | {@const isSelected = suggestion === selectedSuggestion} |
397 | | - {@const isFunction = functions.includes(suggestion)} |
398 | | - {@const isField = !isFunction} |
399 | | - <button type="button" on:click={() => insertSuggestion(suggestion)} class="w-full text-left text-xs font-medium"> |
| 458 | + {@const field = $table.schema.getFieldByIdOrName(suggestion).into(null)} |
| 459 | + <button |
| 460 | + type="button" |
| 461 | + on:mouseenter={() => { |
| 462 | + hoverSuggestion = "" |
| 463 | + }} |
| 464 | + on:click={() => insertSuggestion(suggestion)} |
| 465 | + class="w-full text-left text-xs font-medium" |
| 466 | + > |
400 | 467 | <li class={cn("flex w-full items-center gap-1 p-2 hover:bg-gray-100", isSelected && "bg-gray-100")}> |
401 | | - {#if isFunction} |
402 | | - <span class="font-normal"> |
403 | | - <SquareFunctionIcon class="size-4" /> |
| 468 | + {#if field} |
| 469 | + <span class="flex items-center gap-1"> |
| 470 | + <FieldIcon class="size-4" type={field.type} {field} /> |
| 471 | + {field.name.value} |
404 | 472 | </span> |
405 | | - <span> |
406 | | - {suggestion}() |
407 | | - </span> |
408 | | - {:else} |
409 | | - {@const field = $table.schema.getFieldByIdOrName(suggestion).into(null)} |
410 | | - {#if field} |
411 | | - <span class="flex items-center gap-1"> |
412 | | - <FieldIcon class="size-4" type={field.type} {field} /> |
413 | | - {field.name.value} |
414 | | - </span> |
415 | | - {/if} |
416 | 473 | {/if} |
417 | 474 | </li> |
418 | 475 | </button> |
419 | 476 | {/each} |
420 | 477 | </ul> |
| 478 | + |
| 479 | + <div bind:this={hoverSuggestionContainer} class="fixed left-0 top-0 w-80 rounded-md border bg-white shadow-md"> |
| 480 | + {#if hoverFormula} |
| 481 | + <div class="flex items-center justify-between border-b bg-gray-100 px-2 py-1"> |
| 482 | + <div class="flex items-center gap-2 text-sm"> |
| 483 | + <SquareFunctionIcon class="size-4" /> |
| 484 | + {hoverSuggestion}() |
| 485 | + </div> |
| 486 | + |
| 487 | + {#if hoverFormula.returnType} |
| 488 | + <span |
| 489 | + class="me-2 rounded bg-blue-100 px-2.5 py-0.5 text-xs font-medium uppercase text-blue-800 dark:bg-blue-900 dark:text-blue-300" |
| 490 | + > |
| 491 | + {hoverFormula.returnType} |
| 492 | + </span> |
| 493 | + {/if} |
| 494 | + </div> |
| 495 | + |
| 496 | + <div class="space-y-2 p-2"> |
| 497 | + <p class="overflow-hidden whitespace-normal break-words text-xs text-gray-500">{hoverFormula.description}</p> |
| 498 | + <div class="space-y-2"> |
| 499 | + <p class="text-xs font-semibold text-gray-500">Syntax</p> |
| 500 | + {#each hoverFormula.syntax as syntax} |
| 501 | + <div class="whitespace-normal break-words rounded-sm border px-2 py-1 text-xs leading-6 text-gray-800"> |
| 502 | + {syntax} |
| 503 | + </div> |
| 504 | + {/each} |
| 505 | + </div> |
| 506 | + {#if hoverFormula.examples && hoverFormula.examples.length > 0} |
| 507 | + <p class="text-xs font-semibold text-gray-500">Examples</p> |
| 508 | + <div class="space-y-2"> |
| 509 | + {#each hoverFormula.examples as example} |
| 510 | + <div class="whitespace-normal break-words rounded-sm border px-2 py-1 text-xs leading-6 text-gray-800"> |
| 511 | + {example[0]} |
| 512 | + {#if example[1]} |
| 513 | + <span class="text-gray-500"> |
| 514 | + => {example[1]} |
| 515 | + </span> |
| 516 | + {/if} |
| 517 | + </div> |
| 518 | + {/each} |
| 519 | + </div> |
| 520 | + {/if} |
| 521 | + </div> |
| 522 | + {/if} |
| 523 | + </div> |
421 | 524 | </div> |
422 | 525 |
|
423 | 526 | <style lang="postcss"> |
|
0 commit comments