qwen-code/packages/web-shell/client/extensions/inputHighlight.ts
ermin.zem 5c82857fea
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
Add harness infrastructure for web-shell package (#6517)
* test(web-shell): add browser and lint harness

* test(web-shell): harden browser smoke harness

* fix(web-shell): guard mock daemon model state

* test(web-shell): remove unused scenario harness

* fix(web-shell): remove stale lint disables

* test(web-shell): make matchMedia stub writable

* fix(web-shell): exclude tests from package typecheck

* test(web-shell): tighten mock daemon route contract

* Update packages/web-shell/client/e2e/utils/mockDaemon.ts

Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>

* test(web-shell): clear stale SSE connections

* ci(web-shell): gate smoke on full CI profile

---------

Co-authored-by: ermin.zem <ermin.zem@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-09 08:11:58 +00:00

149 lines
3.9 KiB
TypeScript

import {
ViewPlugin,
Decoration,
EditorView,
WidgetType,
} from '@codemirror/view';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
import type { CommandInfo } from '../adapters/types';
import { getSlashCommandArgumentHint } from '../completions/slashCompletion';
import type { WebShellLanguage } from '../i18n';
const slashDeco = Decoration.mark({ class: 'cm-input-slash' });
const atDeco = Decoration.mark({ class: 'cm-input-at' });
const backtickDeco = Decoration.mark({ class: 'cm-input-code' });
class SlashArgumentHintWidget extends WidgetType {
constructor(private readonly text: string) {
super();
}
eq(other: SlashArgumentHintWidget) {
return other.text === this.text;
}
toDOM() {
const span = document.createElement('span');
span.className = 'cm-input-slash-argument-hint';
span.textContent = this.text;
return span;
}
ignoreEvent() {
return true;
}
}
function buildDecorations(
view: EditorView,
getCommands: () => CommandInfo[],
getLanguage: () => WebShellLanguage,
): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const doc = view.state.doc;
const ranges: Array<{ from: number; to: number; deco: Decoration }> = [];
for (let i = 1; i <= doc.lines; i++) {
const line = doc.line(i);
const text = line.text;
const offset = line.from;
// /command at start of line
if (text.startsWith('/')) {
const end = text.indexOf(' ');
const slashEnd = end === -1 ? text.length : end;
ranges.push({ from: offset, to: offset + slashEnd, deco: slashDeco });
}
const argumentHint = getSlashCommandArgumentHint(
text,
getCommands(),
getLanguage(),
);
if (argumentHint) {
const prefix = text.endsWith(' ') ? '' : ' ';
ranges.push({
from: offset + text.length,
to: offset + text.length,
deco: Decoration.widget({
widget: new SlashArgumentHintWidget(`${prefix}${argumentHint}`),
side: 1,
}),
});
}
// @path tokens
const atRe = /@[^\s]+/g;
let m: RegExpExecArray | null;
while ((m = atRe.exec(text)) !== null) {
ranges.push({
from: offset + m.index,
to: offset + m.index + m[0].length,
deco: atDeco,
});
}
// `inline code`
const codeRe = /`[^`]+`/g;
while ((m = codeRe.exec(text)) !== null) {
ranges.push({
from: offset + m.index,
to: offset + m.index + m[0].length,
deco: backtickDeco,
});
}
}
ranges.sort((a, b) => a.from - b.from || a.to - b.to);
for (const { from, to, deco } of ranges) {
builder.add(from, to, deco);
}
return builder.finish();
}
export function inputHighlight(
getCommands: () => CommandInfo[] = () => [],
getLanguage: () => WebShellLanguage = () => 'en',
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = buildDecorations(view, getCommands, getLanguage);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = buildDecorations(
update.view,
getCommands,
getLanguage,
);
}
}
},
{
decorations: (v) => v.decorations,
},
);
}
export const inputHighlightTheme = EditorView.baseTheme({
'.cm-input-slash': {
color: 'var(--agent-blue-500, #4a9eff)',
fontWeight: 'bold',
},
'.cm-input-at': {
color: 'var(--success-color, #48bb78)',
},
'.cm-input-code': {
background: 'rgba(255, 255, 255, 0.06)',
borderRadius: '3px',
color: 'var(--muted-foreground, #a0aec0)',
},
'.cm-input-slash-argument-hint': {
color: 'color-mix(in srgb, var(--muted-foreground) 70%, transparent)',
pointerEvents: 'none',
},
});