@code-yeongyu/senpi-tui 2026.9.29 → 2026.10.1-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/autocomplete.d.ts +5 -0
- package/dist/autocomplete.js +38 -14
- package/dist/colors.d.ts +73 -0
- package/dist/colors.js +273 -0
- package/dist/components/box.js +5 -3
- package/dist/components/editor.d.ts +4 -1
- package/dist/components/editor.js +23 -8
- package/dist/components/latex.js +5 -0
- package/dist/editor-component.d.ts +7 -2
- package/dist/fuzzy.js +23 -22
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/native-platform.d.ts +2 -0
- package/dist/oklab.d.ts +43 -0
- package/dist/oklab.js +210 -0
- package/dist/slash-command-autocomplete.js +3 -0
- package/dist/terminal-capabilities.js +1 -1
- package/dist/terminal-colors.d.ts +19 -2
- package/dist/terminal-colors.js +12 -7
- package/dist/terminal-image.d.ts +3 -1
- package/dist/terminal-image.js +33 -9
- package/dist/terminal.d.ts +3 -0
- package/dist/terminal.js +15 -8
- package/dist/tui-alt-screen.d.ts +12 -7
- package/dist/tui-alt-screen.js +37 -17
- package/dist/tui.d.ts +18 -17
- package/dist/tui.js +79 -62
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +108 -60
- package/dist/wheel-scroll.d.ts +23 -0
- package/dist/wheel-scroll.js +70 -0
- package/native/clipboard.h +40 -2
- package/native/darwin/README.md +1 -1
- package/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/native/darwin/src/darwin-platform.m +39 -0
- package/native/napi.h +2 -0
- package/package.json +3 -2
- package/dist/alt-screen-search.d.ts.map +0 -1
- package/dist/alt-screen-search.js.map +0 -1
- package/dist/autocomplete.d.ts.map +0 -1
- package/dist/autocomplete.js.map +0 -1
- package/dist/components/alt-screen-flash.d.ts.map +0 -1
- package/dist/components/alt-screen-flash.js.map +0 -1
- package/dist/components/box.d.ts.map +0 -1
- package/dist/components/box.js.map +0 -1
- package/dist/components/cancellable-loader.d.ts.map +0 -1
- package/dist/components/cancellable-loader.js.map +0 -1
- package/dist/components/editor-line-render.d.ts.map +0 -1
- package/dist/components/editor-line-render.js.map +0 -1
- package/dist/components/editor.d.ts.map +0 -1
- package/dist/components/editor.js.map +0 -1
- package/dist/components/h-stack.d.ts.map +0 -1
- package/dist/components/h-stack.js.map +0 -1
- package/dist/components/image.d.ts.map +0 -1
- package/dist/components/image.js.map +0 -1
- package/dist/components/input.d.ts.map +0 -1
- package/dist/components/input.js.map +0 -1
- package/dist/components/latex.d.ts.map +0 -1
- package/dist/components/latex.js.map +0 -1
- package/dist/components/loader.d.ts.map +0 -1
- package/dist/components/loader.js.map +0 -1
- package/dist/components/markdown.d.ts.map +0 -1
- package/dist/components/markdown.js.map +0 -1
- package/dist/components/mouse-region.d.ts.map +0 -1
- package/dist/components/mouse-region.js.map +0 -1
- package/dist/components/scroll-view.d.ts.map +0 -1
- package/dist/components/scroll-view.js.map +0 -1
- package/dist/components/select-list.d.ts.map +0 -1
- package/dist/components/select-list.js.map +0 -1
- package/dist/components/settings-list.d.ts.map +0 -1
- package/dist/components/settings-list.js.map +0 -1
- package/dist/components/spacer.d.ts.map +0 -1
- package/dist/components/spacer.js.map +0 -1
- package/dist/components/stack.d.ts.map +0 -1
- package/dist/components/stack.js.map +0 -1
- package/dist/components/text.d.ts.map +0 -1
- package/dist/components/text.js.map +0 -1
- package/dist/components/truncated-text.d.ts.map +0 -1
- package/dist/components/truncated-text.js.map +0 -1
- package/dist/components/v-stack.d.ts.map +0 -1
- package/dist/components/v-stack.js.map +0 -1
- package/dist/dollar-invocation-autocomplete.d.ts.map +0 -1
- package/dist/dollar-invocation-autocomplete.js.map +0 -1
- package/dist/editor-component.d.ts.map +0 -1
- package/dist/editor-component.js.map +0 -1
- package/dist/fuzzy.d.ts.map +0 -1
- package/dist/fuzzy.js.map +0 -1
- package/dist/image-markers.d.ts.map +0 -1
- package/dist/image-markers.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keybindings.d.ts.map +0 -1
- package/dist/keybindings.js.map +0 -1
- package/dist/keys.d.ts.map +0 -1
- package/dist/keys.js.map +0 -1
- package/dist/kill-ring.d.ts.map +0 -1
- package/dist/kill-ring.js.map +0 -1
- package/dist/layout-node.d.ts.map +0 -1
- package/dist/layout-node.js.map +0 -1
- package/dist/layout.d.ts.map +0 -1
- package/dist/layout.js.map +0 -1
- package/dist/mouse-input.d.ts.map +0 -1
- package/dist/mouse-input.js.map +0 -1
- package/dist/mux.d.ts.map +0 -1
- package/dist/mux.js.map +0 -1
- package/dist/native-modifiers.d.ts.map +0 -1
- package/dist/native-modifiers.js.map +0 -1
- package/dist/native-module-path.d.ts.map +0 -1
- package/dist/native-module-path.js.map +0 -1
- package/dist/native-platform.d.ts.map +0 -1
- package/dist/native-platform.js.map +0 -1
- package/dist/paste-markers.d.ts.map +0 -1
- package/dist/paste-markers.js.map +0 -1
- package/dist/slash-command-autocomplete.d.ts.map +0 -1
- package/dist/slash-command-autocomplete.js.map +0 -1
- package/dist/stderr-observer.d.ts.map +0 -1
- package/dist/stderr-observer.js.map +0 -1
- package/dist/stdin-buffer.d.ts.map +0 -1
- package/dist/stdin-buffer.js.map +0 -1
- package/dist/terminal-capabilities.d.ts.map +0 -1
- package/dist/terminal-capabilities.js.map +0 -1
- package/dist/terminal-colors.d.ts.map +0 -1
- package/dist/terminal-colors.js.map +0 -1
- package/dist/terminal-image.d.ts.map +0 -1
- package/dist/terminal-image.js.map +0 -1
- package/dist/terminal-text.d.ts.map +0 -1
- package/dist/terminal-text.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/tmux-cursor-query.d.ts.map +0 -1
- package/dist/tmux-cursor-query.js.map +0 -1
- package/dist/tmux-focus.d.ts.map +0 -1
- package/dist/tmux-focus.js.map +0 -1
- package/dist/tmux-image-capability.d.ts.map +0 -1
- package/dist/tmux-image-capability.js.map +0 -1
- package/dist/tmux-image-probe.d.ts.map +0 -1
- package/dist/tmux-image-probe.js.map +0 -1
- package/dist/tui-alt-screen.d.ts.map +0 -1
- package/dist/tui-alt-screen.js.map +0 -1
- package/dist/tui-main-screen.d.ts.map +0 -1
- package/dist/tui-main-screen.js.map +0 -1
- package/dist/tui.d.ts.map +0 -1
- package/dist/tui.js.map +0 -1
- package/dist/undo-stack.d.ts.map +0 -1
- package/dist/undo-stack.js.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js.map +0 -1
- package/dist/word-navigation.d.ts.map +0 -1
- package/dist/word-navigation.js.map +0 -1
package/README.md
CHANGED
|
@@ -760,6 +760,44 @@ const lines = wrapTextWithAnsi("This is a long line that needs wrapping", 20);
|
|
|
760
760
|
// ["This is a long line", "that needs wrapping"]
|
|
761
761
|
```
|
|
762
762
|
|
|
763
|
+
### Colors and terminal styles
|
|
764
|
+
|
|
765
|
+
Colors are values that can be converted or mixed before terminal rendering:
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
import {
|
|
769
|
+
colorToRgb,
|
|
770
|
+
foregroundAnsi,
|
|
771
|
+
getTerminalColorMode,
|
|
772
|
+
mixColors,
|
|
773
|
+
parseColor,
|
|
774
|
+
rgbColor,
|
|
775
|
+
styleText,
|
|
776
|
+
} from "@earendil-works/pi-tui";
|
|
777
|
+
|
|
778
|
+
const accent = parseColor("oklch(70% 0.12 220)");
|
|
779
|
+
const background = parseColor("#20242a");
|
|
780
|
+
const foreground = mixColors(accent, background, 0.2);
|
|
781
|
+
|
|
782
|
+
const text = styleText(
|
|
783
|
+
"Ready",
|
|
784
|
+
{ fg: foreground, bg: background, bold: true },
|
|
785
|
+
getTerminalColorMode(),
|
|
786
|
+
);
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
`Color` is an indexed ANSI color, an sRGB color, or an OKLCH color. Every color converts to sRGB, so color math such as `mixColors()` always works. Indices 0-15 follow the user's terminal palette, so their sRGB values are approximations. `styleText()` converts colors to truecolor or 256-color output based on the requested terminal mode.
|
|
790
|
+
|
|
791
|
+
`parseColor()` also accepts OKHSL, as in `okhsl(250 60% 55%)`; `okhslColor()` builds it in code and `colorToOkhsl()` reads any color's OKHSL channels. OKHSL saturation is relative to the most the sRGB gamut allows at that hue and lightness, so every value is in gamut. OKHSL colors are converted to sRGB when created.
|
|
792
|
+
|
|
793
|
+
Conversions are not cached, and OKLCH colors outside the sRGB gamut are the most expensive to convert. For colors used on every render, convert once and reuse the result:
|
|
794
|
+
|
|
795
|
+
```typescript
|
|
796
|
+
const { r, g, b } = colorToRgb(mixColors(accent, background, 0.2));
|
|
797
|
+
const foreground = rgbColor(r, g, b); // cheap to render repeatedly
|
|
798
|
+
const foregroundCode = foregroundAnsi(foreground, getTerminalColorMode());
|
|
799
|
+
```
|
|
800
|
+
|
|
763
801
|
## Creating Custom Components
|
|
764
802
|
|
|
765
803
|
When creating custom components, **each line returned by `render()` must not exceed the `width` parameter**. The TUI will error if any line is wider than the terminal.
|
package/dist/autocomplete.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ export interface AutocompleteItem {
|
|
|
2
2
|
value: string;
|
|
3
3
|
label: string;
|
|
4
4
|
description?: string;
|
|
5
|
+
/**
|
|
6
|
+
* The command declares an argument hint: confirming the row completes `/name ` and waits for
|
|
7
|
+
* arguments instead of submitting.
|
|
8
|
+
*/
|
|
9
|
+
awaitsArguments?: boolean;
|
|
5
10
|
}
|
|
6
11
|
type Awaitable<T> = T | Promise<T>;
|
|
7
12
|
export interface SlashCommand {
|
package/dist/autocomplete.js
CHANGED
|
@@ -4,7 +4,11 @@ import { homedir } from "os";
|
|
|
4
4
|
import { basename, dirname, join } from "path";
|
|
5
5
|
import { findDollarSkillMentions, getDollarInvocationContext, getDollarInvocationSuggestions, knownSkillNames, } from "./dollar-invocation-autocomplete.js";
|
|
6
6
|
import { getSlashCommandSuggestions, isSlashNamespaceItem } from "./slash-command-autocomplete.js";
|
|
7
|
+
import { autocompleteBoundaryRegex, autocompleteSeparatorRegex } from "./utils.js";
|
|
7
8
|
const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
9
|
+
const tokenStartRegex = new RegExp(`${autocompleteBoundaryRegex.source}$`, "u");
|
|
10
|
+
// Opening wrappers that may precede a path in prose, mapped to their closing counterpart.
|
|
11
|
+
const PATH_WRAPPERS = { "(": ")", "[": "]", "{": "}", "<": ">", "`": "`" };
|
|
8
12
|
function toDisplayPath(value) {
|
|
9
13
|
return value.replace(/\\/g, "/");
|
|
10
14
|
}
|
|
@@ -36,12 +40,28 @@ function buildFdPathQuery(query) {
|
|
|
36
40
|
return pattern;
|
|
37
41
|
}
|
|
38
42
|
function findLastDelimiter(text) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
let lastDelimiter = -1;
|
|
44
|
+
let index = 0;
|
|
45
|
+
for (const character of text) {
|
|
46
|
+
index += character.length;
|
|
47
|
+
if (PATH_DELIMITERS.has(character) || autocompleteSeparatorRegex.test(character)) {
|
|
48
|
+
lastDelimiter = index - 1;
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
|
-
return
|
|
51
|
+
return lastDelimiter;
|
|
52
|
+
}
|
|
53
|
+
// Strip opening wrappers before a path, e.g. "(~/Dev" -> "~/Dev" or "`src/ma" -> "src/ma".
|
|
54
|
+
// Keep a wrapper if the token also contains its closer, e.g. "app/[slug]/pa" or "(group)/pa".
|
|
55
|
+
function stripLeadingWrappers(token) {
|
|
56
|
+
let result = token;
|
|
57
|
+
while (result.length > 0) {
|
|
58
|
+
const closer = PATH_WRAPPERS[result[0]];
|
|
59
|
+
if (!closer || result.includes(closer, 1)) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
result = result.slice(1);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
45
65
|
}
|
|
46
66
|
function findUnclosedQuoteStart(text) {
|
|
47
67
|
let inQuotes = false;
|
|
@@ -57,7 +77,11 @@ function findUnclosedQuoteStart(text) {
|
|
|
57
77
|
return inQuotes ? quoteStart : null;
|
|
58
78
|
}
|
|
59
79
|
function isTokenStart(text, index) {
|
|
60
|
-
|
|
80
|
+
let start = index;
|
|
81
|
+
while (start > 0 && PATH_WRAPPERS[text[start - 1]]) {
|
|
82
|
+
start -= 1;
|
|
83
|
+
}
|
|
84
|
+
return PATH_DELIMITERS.has(text[start - 1] ?? "") || tokenStartRegex.test(text.slice(0, start));
|
|
61
85
|
}
|
|
62
86
|
function extractQuotedPrefix(text) {
|
|
63
87
|
const quoteStart = findUnclosedQuoteStart(text);
|
|
@@ -88,7 +112,7 @@ function parsePathPrefix(prefix) {
|
|
|
88
112
|
return { rawPrefix: prefix, isAtPrefix: false, isQuotedPrefix: false };
|
|
89
113
|
}
|
|
90
114
|
function buildCompletionValue(path, options) {
|
|
91
|
-
const needsQuotes = options.isQuotedPrefix ||
|
|
115
|
+
const needsQuotes = options.isQuotedPrefix || autocompleteSeparatorRegex.test(path);
|
|
92
116
|
const prefix = options.isAtPrefix ? "@" : "";
|
|
93
117
|
if (!needsQuotes) {
|
|
94
118
|
return `${prefix}${path}`;
|
|
@@ -377,9 +401,9 @@ export class CombinedAutocompleteProvider {
|
|
|
377
401
|
return quotedPrefix;
|
|
378
402
|
}
|
|
379
403
|
const lastDelimiterIndex = findLastDelimiter(text);
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
382
|
-
return
|
|
404
|
+
const token = stripLeadingWrappers(lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1));
|
|
405
|
+
if (token.startsWith("@")) {
|
|
406
|
+
return token;
|
|
383
407
|
}
|
|
384
408
|
return null;
|
|
385
409
|
}
|
|
@@ -390,7 +414,7 @@ export class CombinedAutocompleteProvider {
|
|
|
390
414
|
return quotedPrefix;
|
|
391
415
|
}
|
|
392
416
|
const lastDelimiterIndex = findLastDelimiter(text);
|
|
393
|
-
const pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);
|
|
417
|
+
const pathPrefix = stripLeadingWrappers(lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1));
|
|
394
418
|
// For forced extraction (Tab key), always return something
|
|
395
419
|
if (forceExtract) {
|
|
396
420
|
return pathPrefix;
|
|
@@ -400,9 +424,9 @@ export class CombinedAutocompleteProvider {
|
|
|
400
424
|
if (pathPrefix.includes("/") || pathPrefix.startsWith(".") || pathPrefix.startsWith("~/")) {
|
|
401
425
|
return pathPrefix;
|
|
402
426
|
}
|
|
403
|
-
// Return empty
|
|
427
|
+
// Return an empty prefix after whitespace or CJK punctuation, but not for empty text.
|
|
404
428
|
// Empty text should not trigger file suggestions - that's for forced Tab completion
|
|
405
|
-
if (pathPrefix === "" && text.
|
|
429
|
+
if (pathPrefix === "" && text !== "" && tokenStartRegex.test(text)) {
|
|
406
430
|
return pathPrefix;
|
|
407
431
|
}
|
|
408
432
|
return null;
|
|
@@ -576,8 +600,8 @@ export class CombinedAutocompleteProvider {
|
|
|
576
600
|
}
|
|
577
601
|
// Sort directories first, then alphabetically
|
|
578
602
|
suggestions.sort((a, b) => {
|
|
579
|
-
const aIsDir = a.
|
|
580
|
-
const bIsDir = b.
|
|
603
|
+
const aIsDir = a.label.endsWith("/");
|
|
604
|
+
const bIsDir = b.label.endsWith("/");
|
|
581
605
|
if (aIsDir && !bIsDir)
|
|
582
606
|
return -1;
|
|
583
607
|
if (!aIsDir && bIsDir)
|
package/dist/colors.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { RgbColor } from "./terminal-colors.ts";
|
|
2
|
+
export interface IndexedColor {
|
|
3
|
+
readonly kind: "indexed";
|
|
4
|
+
readonly index: number;
|
|
5
|
+
}
|
|
6
|
+
export interface RgbColorValue {
|
|
7
|
+
readonly kind: "rgb";
|
|
8
|
+
readonly r: number;
|
|
9
|
+
readonly g: number;
|
|
10
|
+
readonly b: number;
|
|
11
|
+
}
|
|
12
|
+
export interface OklchColorValue {
|
|
13
|
+
readonly kind: "oklch";
|
|
14
|
+
readonly l: number;
|
|
15
|
+
readonly c: number;
|
|
16
|
+
readonly h: number;
|
|
17
|
+
}
|
|
18
|
+
/** A concrete color. Every color can be converted to sRGB, so color math never fails. */
|
|
19
|
+
export type Color = IndexedColor | RgbColorValue | OklchColorValue;
|
|
20
|
+
export type TerminalColorMode = "256color" | "truecolor";
|
|
21
|
+
export type ColorMixSpace = "oklch" | "srgb";
|
|
22
|
+
export interface OklchChannels {
|
|
23
|
+
l: number;
|
|
24
|
+
c: number;
|
|
25
|
+
h: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* OKHSL channels: hue in degrees, saturation and lightness 0-1. Saturation is relative to the most the
|
|
29
|
+
* sRGB gamut allows at that hue and lightness, so every value is in gamut.
|
|
30
|
+
*/
|
|
31
|
+
export interface OkhslChannels {
|
|
32
|
+
h: number;
|
|
33
|
+
s: number;
|
|
34
|
+
l: number;
|
|
35
|
+
}
|
|
36
|
+
export interface TextAttributes {
|
|
37
|
+
bold?: boolean;
|
|
38
|
+
dim?: boolean;
|
|
39
|
+
italic?: boolean;
|
|
40
|
+
underline?: boolean;
|
|
41
|
+
inverse?: boolean;
|
|
42
|
+
strikethrough?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface TextStyle extends TextAttributes {
|
|
45
|
+
fg?: Color;
|
|
46
|
+
bg?: Color;
|
|
47
|
+
}
|
|
48
|
+
export declare function indexedColor(index: number): IndexedColor;
|
|
49
|
+
export declare function rgbColor(r: number, g: number, b: number): RgbColorValue;
|
|
50
|
+
export declare function oklchColor(l: number, c: number, h: number): OklchColorValue;
|
|
51
|
+
/**
|
|
52
|
+
* An OKHSL color, converted to sRGB. Saturation is relative to the sRGB gamut at the hue and lightness,
|
|
53
|
+
* so equal saturation looks equally colorful across hues and lightness.
|
|
54
|
+
* @param h Hue in degrees.
|
|
55
|
+
* @param s Saturation, 0-1.
|
|
56
|
+
* @param l Lightness, 0-1.
|
|
57
|
+
*/
|
|
58
|
+
export declare function okhslColor(h: number, s: number, l: number): RgbColorValue;
|
|
59
|
+
export declare function colorToOkhsl(color: Color): OkhslChannels;
|
|
60
|
+
export declare function parseColor(value: string | number): Color;
|
|
61
|
+
export declare function colorToRgb(color: Color): RgbColor;
|
|
62
|
+
export declare function colorToOklch(color: Color): OklchChannels;
|
|
63
|
+
export declare function colorToHex(color: Color): string;
|
|
64
|
+
export declare function mixColors(first: Color, second: Color, amount: number, space?: ColorMixSpace): Color;
|
|
65
|
+
export declare function foregroundAnsi(color: Color, mode: TerminalColorMode): string;
|
|
66
|
+
export declare function backgroundAnsi(color: Color, mode: TerminalColorMode): string;
|
|
67
|
+
export declare function styleText(text: string, options: TextStyle, mode: TerminalColorMode): string;
|
|
68
|
+
/**
|
|
69
|
+
* Like `styleText()`, but with precomputed color escape sequences, e.g. cached theme colors.
|
|
70
|
+
* Colors in `options` are ignored.
|
|
71
|
+
*/
|
|
72
|
+
export declare function styleTextWithAnsi(text: string, fgAnsi: string | undefined, bgAnsi: string | undefined, options: TextAttributes): string;
|
|
73
|
+
//# sourceMappingURL=colors.d.ts.map
|
package/dist/colors.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { linearSrgbToRgb, okhslToRgb, oklabToLinearSrgb, rgbToOkhsl, rgbToOklab } from "./oklab.js";
|
|
2
|
+
function requireFinite(value, name) {
|
|
3
|
+
if (!Number.isFinite(value))
|
|
4
|
+
throw new Error(`${name} must be finite`);
|
|
5
|
+
}
|
|
6
|
+
export function indexedColor(index) {
|
|
7
|
+
if (!Number.isInteger(index) || index < 0 || index > 255) {
|
|
8
|
+
throw new Error(`ANSI color index must be an integer from 0 to 255: ${index}`);
|
|
9
|
+
}
|
|
10
|
+
return Object.freeze({ kind: "indexed", index });
|
|
11
|
+
}
|
|
12
|
+
export function rgbColor(r, g, b) {
|
|
13
|
+
for (const [name, value] of [
|
|
14
|
+
["r", r],
|
|
15
|
+
["g", g],
|
|
16
|
+
["b", b],
|
|
17
|
+
]) {
|
|
18
|
+
requireFinite(value, name);
|
|
19
|
+
if (value < 0 || value > 255)
|
|
20
|
+
throw new Error(`${name} must be between 0 and 255: ${value}`);
|
|
21
|
+
}
|
|
22
|
+
return Object.freeze({ kind: "rgb", r, g, b });
|
|
23
|
+
}
|
|
24
|
+
export function oklchColor(l, c, h) {
|
|
25
|
+
requireFinite(l, "l");
|
|
26
|
+
requireFinite(c, "c");
|
|
27
|
+
requireFinite(h, "h");
|
|
28
|
+
if (l < 0 || l > 1)
|
|
29
|
+
throw new Error(`l must be between 0 and 1: ${l}`);
|
|
30
|
+
if (c < 0)
|
|
31
|
+
throw new Error(`c must not be negative: ${c}`);
|
|
32
|
+
return Object.freeze({ kind: "oklch", l, c, h: ((h % 360) + 360) % 360 });
|
|
33
|
+
}
|
|
34
|
+
const NUMBER_PATTERN = String.raw `[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?`;
|
|
35
|
+
const OKLCH_PATTERN = new RegExp(`^oklch\\(\\s*(${NUMBER_PATTERN})(%)?\\s+(${NUMBER_PATTERN})\\s+(${NUMBER_PATTERN})(?:deg)?\\s*\\)$`, "i");
|
|
36
|
+
const OKHSL_PATTERN = new RegExp(`^okhsl\\(\\s*(${NUMBER_PATTERN})(?:deg)?\\s+(${NUMBER_PATTERN})(%)?\\s+(${NUMBER_PATTERN})(%)?\\s*\\)$`, "i");
|
|
37
|
+
/**
|
|
38
|
+
* An OKHSL color, converted to sRGB. Saturation is relative to the sRGB gamut at the hue and lightness,
|
|
39
|
+
* so equal saturation looks equally colorful across hues and lightness.
|
|
40
|
+
* @param h Hue in degrees.
|
|
41
|
+
* @param s Saturation, 0-1.
|
|
42
|
+
* @param l Lightness, 0-1.
|
|
43
|
+
*/
|
|
44
|
+
export function okhslColor(h, s, l) {
|
|
45
|
+
requireFinite(h, "h");
|
|
46
|
+
requireFinite(s, "s");
|
|
47
|
+
requireFinite(l, "l");
|
|
48
|
+
if (s < 0 || s > 1)
|
|
49
|
+
throw new Error(`s must be between 0 and 1: ${s}`);
|
|
50
|
+
if (l < 0 || l > 1)
|
|
51
|
+
throw new Error(`l must be between 0 and 1: ${l}`);
|
|
52
|
+
const { r, g, b } = okhslToRgb(h, s, l);
|
|
53
|
+
return rgbColor(r, g, b);
|
|
54
|
+
}
|
|
55
|
+
export function colorToOkhsl(color) {
|
|
56
|
+
return rgbToOkhsl(colorToRgb(color));
|
|
57
|
+
}
|
|
58
|
+
export function parseColor(value) {
|
|
59
|
+
if (typeof value === "number")
|
|
60
|
+
return indexedColor(value);
|
|
61
|
+
const hex = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(value);
|
|
62
|
+
if (hex) {
|
|
63
|
+
const digits = hex[1].length === 3 ? [...hex[1]].map((digit) => digit + digit).join("") : hex[1];
|
|
64
|
+
return rgbColor(Number.parseInt(digits.slice(0, 2), 16), Number.parseInt(digits.slice(2, 4), 16), Number.parseInt(digits.slice(4, 6), 16));
|
|
65
|
+
}
|
|
66
|
+
const oklch = OKLCH_PATTERN.exec(value);
|
|
67
|
+
if (oklch) {
|
|
68
|
+
const lightness = Number.parseFloat(oklch[1]) / (oklch[2] ? 100 : 1);
|
|
69
|
+
return oklchColor(lightness, Number.parseFloat(oklch[3]), Number.parseFloat(oklch[4]));
|
|
70
|
+
}
|
|
71
|
+
const okhsl = OKHSL_PATTERN.exec(value);
|
|
72
|
+
if (okhsl) {
|
|
73
|
+
const saturation = Number.parseFloat(okhsl[2]) / (okhsl[3] ? 100 : 1);
|
|
74
|
+
const lightness = Number.parseFloat(okhsl[4]) / (okhsl[5] ? 100 : 1);
|
|
75
|
+
return okhslColor(Number.parseFloat(okhsl[1]), saturation, lightness);
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Invalid color value: ${value}`);
|
|
78
|
+
}
|
|
79
|
+
const BASIC_COLORS = [
|
|
80
|
+
{ r: 0, g: 0, b: 0 },
|
|
81
|
+
{ r: 128, g: 0, b: 0 },
|
|
82
|
+
{ r: 0, g: 128, b: 0 },
|
|
83
|
+
{ r: 128, g: 128, b: 0 },
|
|
84
|
+
{ r: 0, g: 0, b: 128 },
|
|
85
|
+
{ r: 128, g: 0, b: 128 },
|
|
86
|
+
{ r: 0, g: 128, b: 128 },
|
|
87
|
+
{ r: 192, g: 192, b: 192 },
|
|
88
|
+
{ r: 128, g: 128, b: 128 },
|
|
89
|
+
{ r: 255, g: 0, b: 0 },
|
|
90
|
+
{ r: 0, g: 255, b: 0 },
|
|
91
|
+
{ r: 255, g: 255, b: 0 },
|
|
92
|
+
{ r: 0, g: 0, b: 255 },
|
|
93
|
+
{ r: 255, g: 0, b: 255 },
|
|
94
|
+
{ r: 0, g: 255, b: 255 },
|
|
95
|
+
{ r: 255, g: 255, b: 255 },
|
|
96
|
+
];
|
|
97
|
+
const CUBE_VALUES = [0, 95, 135, 175, 215, 255];
|
|
98
|
+
const GRAY_VALUES = Array.from({ length: 24 }, (_, index) => 8 + index * 10);
|
|
99
|
+
function indexedToRgb(index) {
|
|
100
|
+
if (index < 16)
|
|
101
|
+
return { ...BASIC_COLORS[index] };
|
|
102
|
+
if (index < 232) {
|
|
103
|
+
const cubeIndex = index - 16;
|
|
104
|
+
return {
|
|
105
|
+
r: CUBE_VALUES[Math.floor(cubeIndex / 36)],
|
|
106
|
+
g: CUBE_VALUES[Math.floor((cubeIndex % 36) / 6)],
|
|
107
|
+
b: CUBE_VALUES[cubeIndex % 6],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const gray = 8 + (index - 232) * 10;
|
|
111
|
+
return { r: gray, g: gray, b: gray };
|
|
112
|
+
}
|
|
113
|
+
function isInSrgbGamut(linear) {
|
|
114
|
+
const epsilon = 1e-7;
|
|
115
|
+
return linear.every((channel) => channel >= -epsilon && channel <= 1 + epsilon);
|
|
116
|
+
}
|
|
117
|
+
function oklchToRgb({ l, c, h }) {
|
|
118
|
+
// Gamut mapping keeps the hue fixed, so its direction is computed once and scaled by chroma.
|
|
119
|
+
const radians = (h * Math.PI) / 180;
|
|
120
|
+
const cos = Math.cos(radians);
|
|
121
|
+
const sin = Math.sin(radians);
|
|
122
|
+
const atChroma = (chroma) => oklabToLinearSrgb([l, chroma * cos, chroma * sin]);
|
|
123
|
+
const direct = atChroma(c);
|
|
124
|
+
if (isInSrgbGamut(direct))
|
|
125
|
+
return linearSrgbToRgb(direct);
|
|
126
|
+
// Reduce chroma until the color fits. The achromatic color is always in gamut, so it is the
|
|
127
|
+
// fallback when no bisection step fits, e.g. `oklch(100% 0.3 150)` must map to white.
|
|
128
|
+
let linear = atChroma(0);
|
|
129
|
+
let low = 0;
|
|
130
|
+
let high = c;
|
|
131
|
+
for (let index = 0; index < 20; index++) {
|
|
132
|
+
const chroma = (low + high) / 2;
|
|
133
|
+
const candidate = atChroma(chroma);
|
|
134
|
+
if (isInSrgbGamut(candidate)) {
|
|
135
|
+
low = chroma;
|
|
136
|
+
linear = candidate;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
high = chroma;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return linearSrgbToRgb(linear);
|
|
143
|
+
}
|
|
144
|
+
export function colorToRgb(color) {
|
|
145
|
+
switch (color.kind) {
|
|
146
|
+
case "indexed":
|
|
147
|
+
return indexedToRgb(color.index);
|
|
148
|
+
case "rgb":
|
|
149
|
+
return { r: color.r, g: color.g, b: color.b };
|
|
150
|
+
case "oklch":
|
|
151
|
+
return oklchToRgb(color);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function colorToOklch(color) {
|
|
155
|
+
if (color.kind === "oklch")
|
|
156
|
+
return { l: color.l, c: color.c, h: color.h };
|
|
157
|
+
const [l, a, b] = rgbToOklab(colorToRgb(color));
|
|
158
|
+
return { l, c: Math.hypot(a, b), h: ((Math.atan2(b, a) * 180) / Math.PI + 360) % 360 };
|
|
159
|
+
}
|
|
160
|
+
export function colorToHex(color) {
|
|
161
|
+
const { r, g, b } = colorToRgb(color);
|
|
162
|
+
const channel = (value) => Math.round(value).toString(16).padStart(2, "0");
|
|
163
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`;
|
|
164
|
+
}
|
|
165
|
+
export function mixColors(first, second, amount, space = "oklch") {
|
|
166
|
+
requireFinite(amount, "amount");
|
|
167
|
+
if (amount < 0 || amount > 1)
|
|
168
|
+
throw new Error(`amount must be between 0 and 1: ${amount}`);
|
|
169
|
+
if (space === "srgb") {
|
|
170
|
+
const a = colorToRgb(first);
|
|
171
|
+
const b = colorToRgb(second);
|
|
172
|
+
return rgbColor(a.r + (b.r - a.r) * amount, a.g + (b.g - a.g) * amount, a.b + (b.b - a.b) * amount);
|
|
173
|
+
}
|
|
174
|
+
const a = colorToOklch(first);
|
|
175
|
+
const b = colorToOklch(second);
|
|
176
|
+
const firstHue = a.c < 1e-7 ? b.h : a.h;
|
|
177
|
+
const secondHue = b.c < 1e-7 ? firstHue : b.h;
|
|
178
|
+
const hueDelta = ((secondHue - firstHue + 540) % 360) - 180;
|
|
179
|
+
return oklchColor(a.l + (b.l - a.l) * amount, a.c + (b.c - a.c) * amount, firstHue + hueDelta * amount);
|
|
180
|
+
}
|
|
181
|
+
function findClosest(values, target) {
|
|
182
|
+
let closestIndex = 0;
|
|
183
|
+
let closestDistance = Infinity;
|
|
184
|
+
for (let index = 0; index < values.length; index++) {
|
|
185
|
+
const distance = Math.abs(target - values[index]);
|
|
186
|
+
if (distance < closestDistance) {
|
|
187
|
+
closestIndex = index;
|
|
188
|
+
closestDistance = distance;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return closestIndex;
|
|
192
|
+
}
|
|
193
|
+
function colorDistance(first, second) {
|
|
194
|
+
const dr = first.r - second.r;
|
|
195
|
+
const dg = first.g - second.g;
|
|
196
|
+
const db = first.b - second.b;
|
|
197
|
+
return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114;
|
|
198
|
+
}
|
|
199
|
+
function rgbToAnsi256(color) {
|
|
200
|
+
const rIndex = findClosest(CUBE_VALUES, color.r);
|
|
201
|
+
const gIndex = findClosest(CUBE_VALUES, color.g);
|
|
202
|
+
const bIndex = findClosest(CUBE_VALUES, color.b);
|
|
203
|
+
const cubeColor = { r: CUBE_VALUES[rIndex], g: CUBE_VALUES[gIndex], b: CUBE_VALUES[bIndex] };
|
|
204
|
+
const cubeIndex = 16 + 36 * rIndex + 6 * gIndex + bIndex;
|
|
205
|
+
const gray = Math.round(0.299 * color.r + 0.587 * color.g + 0.114 * color.b);
|
|
206
|
+
const grayOffset = findClosest(GRAY_VALUES, gray);
|
|
207
|
+
const grayValue = GRAY_VALUES[grayOffset];
|
|
208
|
+
const spread = Math.max(color.r, color.g, color.b) - Math.min(color.r, color.g, color.b);
|
|
209
|
+
if (spread < 10 &&
|
|
210
|
+
colorDistance(color, { r: grayValue, g: grayValue, b: grayValue }) < colorDistance(color, cubeColor)) {
|
|
211
|
+
return 232 + grayOffset;
|
|
212
|
+
}
|
|
213
|
+
return cubeIndex;
|
|
214
|
+
}
|
|
215
|
+
function colorAnsi(color, mode, background) {
|
|
216
|
+
if (color.kind === "indexed")
|
|
217
|
+
return `\x1b[${background ? 48 : 38};5;${color.index}m`;
|
|
218
|
+
const rgb = colorToRgb(color);
|
|
219
|
+
if (mode === "truecolor") {
|
|
220
|
+
return `\x1b[${background ? 48 : 38};2;${Math.round(rgb.r)};${Math.round(rgb.g)};${Math.round(rgb.b)}m`;
|
|
221
|
+
}
|
|
222
|
+
return `\x1b[${background ? 48 : 38};5;${rgbToAnsi256(rgb)}m`;
|
|
223
|
+
}
|
|
224
|
+
export function foregroundAnsi(color, mode) {
|
|
225
|
+
return colorAnsi(color, mode, false);
|
|
226
|
+
}
|
|
227
|
+
export function backgroundAnsi(color, mode) {
|
|
228
|
+
return colorAnsi(color, mode, true);
|
|
229
|
+
}
|
|
230
|
+
export function styleText(text, options, mode) {
|
|
231
|
+
return styleTextWithAnsi(text, options.fg && foregroundAnsi(options.fg, mode), options.bg && backgroundAnsi(options.bg, mode), options);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Like `styleText()`, but with precomputed color escape sequences, e.g. cached theme colors.
|
|
235
|
+
* Colors in `options` are ignored.
|
|
236
|
+
*/
|
|
237
|
+
export function styleTextWithAnsi(text, fgAnsi, bgAnsi, options) {
|
|
238
|
+
// Resets are prepended so they close in reverse order of the opening sequences.
|
|
239
|
+
let prefix = "";
|
|
240
|
+
let suffix = "";
|
|
241
|
+
if (fgAnsi) {
|
|
242
|
+
prefix += fgAnsi;
|
|
243
|
+
suffix = "\x1b[39m";
|
|
244
|
+
}
|
|
245
|
+
if (bgAnsi) {
|
|
246
|
+
prefix += bgAnsi;
|
|
247
|
+
suffix = `\x1b[49m${suffix}`;
|
|
248
|
+
}
|
|
249
|
+
if (options.bold)
|
|
250
|
+
prefix += "\x1b[1m";
|
|
251
|
+
if (options.dim)
|
|
252
|
+
prefix += "\x1b[2m";
|
|
253
|
+
if (options.bold || options.dim)
|
|
254
|
+
suffix = `\x1b[22m${suffix}`;
|
|
255
|
+
if (options.italic) {
|
|
256
|
+
prefix += "\x1b[3m";
|
|
257
|
+
suffix = `\x1b[23m${suffix}`;
|
|
258
|
+
}
|
|
259
|
+
if (options.underline) {
|
|
260
|
+
prefix += "\x1b[4m";
|
|
261
|
+
suffix = `\x1b[24m${suffix}`;
|
|
262
|
+
}
|
|
263
|
+
if (options.inverse) {
|
|
264
|
+
prefix += "\x1b[7m";
|
|
265
|
+
suffix = `\x1b[27m${suffix}`;
|
|
266
|
+
}
|
|
267
|
+
if (options.strikethrough) {
|
|
268
|
+
prefix += "\x1b[9m";
|
|
269
|
+
suffix = `\x1b[29m${suffix}`;
|
|
270
|
+
}
|
|
271
|
+
return `${prefix}${text}${suffix}`;
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=colors.js.map
|
package/dist/components/box.js
CHANGED
|
@@ -94,14 +94,16 @@ export class Box {
|
|
|
94
94
|
}
|
|
95
95
|
const contentWidth = Math.max(1, width - this.paddingX * 2);
|
|
96
96
|
const leftPad = " ".repeat(this.paddingX);
|
|
97
|
-
// Render all children
|
|
97
|
+
// Render all children. Keep the child lines unpadded: children usually return the same string
|
|
98
|
+
// objects every frame, so the cache check below is a cheap identity comparison per line.
|
|
99
|
+
// Padding here would create new strings that must be compared character by character.
|
|
98
100
|
const childLines = [];
|
|
99
101
|
const mouseChildren = [];
|
|
100
102
|
for (const child of this.children) {
|
|
101
103
|
const lines = child.render(contentWidth);
|
|
102
104
|
mouseChildren.push({ component: child, height: lines.length });
|
|
103
105
|
for (const line of lines) {
|
|
104
|
-
childLines.push(
|
|
106
|
+
childLines.push(line);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
this.mouseLayout = { width: contentWidth, children: mouseChildren };
|
|
@@ -122,7 +124,7 @@ export class Box {
|
|
|
122
124
|
}
|
|
123
125
|
// Content
|
|
124
126
|
for (const line of childLines) {
|
|
125
|
-
result.push(this.applyBg(line, width));
|
|
127
|
+
result.push(this.applyBg(leftPad + line, width));
|
|
126
128
|
}
|
|
127
129
|
// Bottom padding
|
|
128
130
|
for (let i = 0; i < this.paddingY; i++) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AutocompleteProvider } from "../autocomplete.ts";
|
|
2
|
+
import type { EditorSubmitDetails } from "../editor-component.ts";
|
|
2
3
|
import { type EditorImageState } from "../image-markers.ts";
|
|
3
4
|
import { type EditorPasteState } from "../paste-markers.ts";
|
|
4
5
|
import { type Component, type Focusable, type TUI, type TuiMouseEvent, type TuiMouseEventResult } from "../tui.ts";
|
|
@@ -68,6 +69,8 @@ export declare class Editor implements Component, Focusable {
|
|
|
68
69
|
private autocompleteList?;
|
|
69
70
|
private autocompleteState;
|
|
70
71
|
private autocompletePrefix;
|
|
72
|
+
/** The provider items behind `autocompleteList`, which only carries the display fields. */
|
|
73
|
+
private autocompleteItems;
|
|
71
74
|
private autocompleteMaxVisible;
|
|
72
75
|
private autocompleteAbort?;
|
|
73
76
|
private autocompleteDebounceTimer?;
|
|
@@ -88,7 +91,7 @@ export declare class Editor implements Component, Focusable {
|
|
|
88
91
|
private snappedFromCursorCol;
|
|
89
92
|
private undoStack;
|
|
90
93
|
private wrappedLineCache;
|
|
91
|
-
onSubmit?: (text: string) => void;
|
|
94
|
+
onSubmit?: (text: string, details?: EditorSubmitDetails) => void;
|
|
92
95
|
onChange?: (text: string) => void;
|
|
93
96
|
/**
|
|
94
97
|
* Fired whenever image markers are added, removed, pruned or renumbered.
|
|
@@ -7,7 +7,7 @@ import { isSlashNamespaceItem } from "../slash-command-autocomplete.js";
|
|
|
7
7
|
import { normalizeWarpWslShiftEnterInput } from "../terminal.js";
|
|
8
8
|
import { CURSOR_MARKER, } from "../tui.js";
|
|
9
9
|
import { UndoStack } from "../undo-stack.js";
|
|
10
|
-
import { cjkBreakRegex, getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth, } from "../utils.js";
|
|
10
|
+
import { autocompleteBoundaryRegex, autocompleteSeparatorRegex, cjkBreakRegex, getGraphemeSegmenter, getWordSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth, } from "../utils.js";
|
|
11
11
|
import { findWordBackward, findWordForward } from "../word-navigation.js";
|
|
12
12
|
import { renderEditorLine } from "./editor-line-render.js";
|
|
13
13
|
import { SelectList } from "./select-list.js";
|
|
@@ -173,15 +173,19 @@ const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
|
|
|
173
173
|
};
|
|
174
174
|
const ATTACHMENT_AUTOCOMPLETE_DEBOUNCE_MS = 20;
|
|
175
175
|
const DEFAULT_AUTOCOMPLETE_TRIGGER_CHARACTERS = ["@", "#", "$"];
|
|
176
|
+
// Unquoted completions end at whitespace or CJK punctuation; quoted paths may contain either.
|
|
177
|
+
const unquotedAutocompleteSuffixRegex = new RegExp(`(?:(?!${autocompleteSeparatorRegex.source}).)*`, "u");
|
|
178
|
+
// Trigger tokens may be wrapped in prose, e.g. "(@src/foo" or "`@src/foo".
|
|
179
|
+
const autocompleteTokenStartSource = `${autocompleteBoundaryRegex.source}[([{<\`]*`;
|
|
176
180
|
function escapeCharacterClass(value) {
|
|
177
181
|
return value.replace(/[\\^$.*+?()[\]{}|-]/g, "\\$&");
|
|
178
182
|
}
|
|
179
183
|
function buildTriggerPattern(triggerCharacters) {
|
|
180
|
-
return new RegExp(
|
|
184
|
+
return new RegExp(`${autocompleteTokenStartSource}(?:@"[^"]*|[${triggerCharacters.map(escapeCharacterClass).join("")}]${unquotedAutocompleteSuffixRegex.source})$`, "u");
|
|
181
185
|
}
|
|
182
186
|
function buildDebouncePattern(triggerCharacters) {
|
|
183
187
|
const escapedWithoutAt = triggerCharacters.filter((character) => character !== "@").map(escapeCharacterClass);
|
|
184
|
-
return new RegExp(
|
|
188
|
+
return new RegExp(`${autocompleteTokenStartSource}(?:@(?:"[^"]*|${unquotedAutocompleteSuffixRegex.source})|[${escapedWithoutAt.join("")}]${unquotedAutocompleteSuffixRegex.source})$`, "u");
|
|
185
189
|
}
|
|
186
190
|
function createScrollBorder(direction, hiddenLineCount, width) {
|
|
187
191
|
const availableWidth = Math.max(0, width);
|
|
@@ -220,6 +224,8 @@ export class Editor {
|
|
|
220
224
|
this.autocompleteDebouncePattern = buildDebouncePattern(this.autocompleteTriggerCharacters);
|
|
221
225
|
this.autocompleteState = null;
|
|
222
226
|
this.autocompletePrefix = "";
|
|
227
|
+
/** The provider items behind `autocompleteList`, which only carries the display fields. */
|
|
228
|
+
this.autocompleteItems = [];
|
|
223
229
|
this.autocompleteMaxVisible = 5;
|
|
224
230
|
this.autocompleteRequestTask = Promise.resolve();
|
|
225
231
|
this.autocompleteStartToken = 0;
|
|
@@ -718,7 +724,14 @@ export class Editor {
|
|
|
718
724
|
return;
|
|
719
725
|
}
|
|
720
726
|
if (this.autocompletePrefix.startsWith("/")) {
|
|
727
|
+
const awaitsArguments = this.autocompleteItems.some((item) => item === selected && item.awaitsArguments === true);
|
|
721
728
|
this.cancelAutocomplete();
|
|
729
|
+
if (awaitsArguments) {
|
|
730
|
+
// The command takes arguments: leave `/name ` in the editor for them.
|
|
731
|
+
if (this.onChange)
|
|
732
|
+
this.onChange(this.getText());
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
722
735
|
// Fall through to submit
|
|
723
736
|
}
|
|
724
737
|
else {
|
|
@@ -1219,13 +1232,12 @@ export class Editor {
|
|
|
1219
1232
|
else if (this.autocompleteTriggerCharacters.includes(char)) {
|
|
1220
1233
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1221
1234
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1222
|
-
|
|
1223
|
-
if (textBeforeCursor.length === 1 || charBeforeSymbol === " " || charBeforeSymbol === "\t") {
|
|
1235
|
+
if (this.autocompleteTriggerPattern.test(textBeforeCursor)) {
|
|
1224
1236
|
this.tryTriggerAutocomplete();
|
|
1225
1237
|
}
|
|
1226
1238
|
}
|
|
1227
1239
|
// Also auto-trigger when typing letters in a slash command or symbol completion context
|
|
1228
|
-
else if (/[a-zA-Z0-9.\-_]/.test(char)) {
|
|
1240
|
+
else if (/[a-zA-Z0-9.\-_]/.test(char) || cjkBreakRegex.test(char)) {
|
|
1229
1241
|
const currentLine = this.state.lines[this.state.cursorLine] || "";
|
|
1230
1242
|
const textBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
1231
1243
|
// Check if we're in a slash command (with or without space for arguments)
|
|
@@ -1325,7 +1337,8 @@ export class Editor {
|
|
|
1325
1337
|
}
|
|
1326
1338
|
submitValue() {
|
|
1327
1339
|
this.cancelAutocomplete();
|
|
1328
|
-
const
|
|
1340
|
+
const rawText = this.pasteMarkers.expand(this.state.lines.join("\n"));
|
|
1341
|
+
const result = rawText.trim();
|
|
1329
1342
|
this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
|
|
1330
1343
|
this.pasteMarkers.clear();
|
|
1331
1344
|
// Marker numbering is turn-local: ids restart at 1 for the next submission.
|
|
@@ -1337,7 +1350,7 @@ export class Editor {
|
|
|
1337
1350
|
if (this.onChange)
|
|
1338
1351
|
this.onChange("");
|
|
1339
1352
|
if (this.onSubmit)
|
|
1340
|
-
this.onSubmit(result);
|
|
1353
|
+
this.onSubmit(result, { rawText });
|
|
1341
1354
|
}
|
|
1342
1355
|
handleBackspace() {
|
|
1343
1356
|
this.exitHistoryBrowsing();
|
|
@@ -2203,6 +2216,7 @@ export class Editor {
|
|
|
2203
2216
|
}
|
|
2204
2217
|
applyAutocompleteSuggestions(suggestions, state) {
|
|
2205
2218
|
this.autocompletePrefix = suggestions.prefix;
|
|
2219
|
+
this.autocompleteItems = suggestions.items;
|
|
2206
2220
|
this.autocompleteList = this.createAutocompleteList(suggestions.prefix, suggestions.items);
|
|
2207
2221
|
const bestMatchIndex = this.getBestAutocompleteMatchIndex(suggestions.items, suggestions.prefix);
|
|
2208
2222
|
if (bestMatchIndex >= 0) {
|
|
@@ -2222,6 +2236,7 @@ export class Editor {
|
|
|
2222
2236
|
clearAutocompleteUi() {
|
|
2223
2237
|
this.autocompleteState = null;
|
|
2224
2238
|
this.autocompleteList = undefined;
|
|
2239
|
+
this.autocompleteItems = [];
|
|
2225
2240
|
this.autocompletePrefix = "";
|
|
2226
2241
|
}
|
|
2227
2242
|
cancelAutocomplete() {
|