Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Editor

Editor — редактор кода: подсветка синтаксиса для более чем пятидесяти языков, номера строк, несколько кареток, сворачивание, мягкий перенос строк, поиск и замена, диагностика и декорации, а также привычные для редакторов кода клавиши (перемещение и дублирование строк, комментарии, отступы, скобки и кавычки, которые закрываются по мере набора). Он работает с файлами любой длины: создаются только видимые строки, а подсветка обновляется построчно после каждой правки, так что длинный файл прокручивается и редактируется так же легко, как короткий. Ввод проходит через платформенный механизм ввода, поэтому методы ввода (пиньинь, кана, «мёртвые» клавиши) и экранные клавиатуры работают так же, как в любом текстовом поле.

el-editor.tsx
el-editor.tsxLive
import { mount, signal, Text, View } from "nsh";
import { Input } from "nsh/base";
import { Editor, type EditorDiagnostic, type EditorRef, type EditorSearchResult } from "nsh/base";
import { button, buttonText, desktop, field, Log, Panel, Segmented, size, Split, ui, wide } from "./palette";

const SAMPLES = {
  rust: `/// The n-th Fibonacci number.
fn fib(n: u64) -> u64 {
    match n {
        0 | 1 => n,
        _ => fib(n - 1) + fib(n - 2),
    }
}

fn main() {
    let unused = 0;
    for n in 0..10 {
        println!("fib({n}) = {}", fib(n));
    }
}
`,
  typescript: `type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.r ** 2;
    case "square":
      return shape.side ** 2;
  }
}

console.log(area({ kind: "circle", r: 2 }));
`,
  json: `{
  "name": "my-app",
  "entry": "App.tsx",
  "capabilities": ["network"]
}
`,
} as const;

type Language = keyof typeof SAMPLES;

// A problem to mark in each file, as a linter would report it.
const PROBLEMS: Record<Language, EditorDiagnostic[]> = {
  rust: [{ range: { start: { line: 9, column: 8 }, end: { line: 9, column: 14 } }, severity: "warning", message: "unused variable" }],
  typescript: [],
  json: [],
};

// The text is the editor's own (`defaultValue`), an editor per language so
// each keeps its own undo; the app passes what to find (`search`) and reads
// the caret and the matches from the events.
function App() {
  const language = signal<Language>("rust");
  const wrap = signal<"Scroll" | "Wrap">("Scroll");
  const query = signal("");
  const caret = signal("1:1");
  const found = signal<EditorSearchResult>({ count: 0 });
  const events = signal<string[]>([]);
  const log = (line: string) => events.set([line, ...events()].slice(0, 4));
  let editor: EditorRef | undefined;

  const action = (label: string, onPress: () => void) => (
    <View onPress={onPress} role="button" {...button("secondary", { size: "sm" })}>
      <Text style={buttonText()}>{label}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>editor</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Click into the code and type; Alt-click adds a caret, Cmd/Ctrl+D the next match" : "Tap into the code and type"}
      </Text>

      <Split
        grow
        width={640}
        inspector={
          <>
            <Segmented label="language" options={["rust", "typescript", "json"] as const} value={language()} onChange={(v) => language.set(v)} />
            <Segmented label="long lines" options={["Scroll", "Wrap"] as const} value={wrap()} onChange={(v) => wrap.set(v)} />
            <Panel title="find">
              <Input
                value={query()}
                onChange={({ value }) => query.set(value)}
                onSubmit={() => editor?.run("findNext")}
                submitBehavior="submit"
                placeholder="fib"
                accessibilityLabel="Find"
                autoCapitalize="none"
                autoCorrect={false}
                {...field({ style: { height: size.controlSm, paddingVertical: 0 } })}
              />
              <Text style={{ fontSize: 13, fontVariant: ["tabular-nums"], color: ui().text }}>
                {query() ? (found().error ?? `${found().current == null ? "–" : found().current! + 1} of ${found().count}`) : "–"}
              </Text>
              <View style={{ flexDirection: "row", gap: 8 }}>
                {action("Next", () => editor?.run("findNext"))}
                {action("Undo", () => editor?.run("undo"))}
              </View>
            </Panel>
            <Panel title="caret">
              <Text style={{ fontSize: 13, fontVariant: ["tabular-nums"], color: ui().text }}>{caret()}</Text>
            </Panel>
            <Log title="events" lines={events()} minHeight={96} />
          </>
        }
      >
        <View style={{ height: wide() ? 380 : 300, ...ui().card, borderRadius: 14, overflow: "hidden" }}>
          {() => {
            const lang = language();
            return (
              <Editor
                ref={(r) => (editor = r)}
                defaultValue={SAMPLES[lang]}
                language={lang}
                softWrap={wrap() === "Wrap"}
                search={{ query: query() }}
                diagnostics={PROBLEMS[lang]}
                accessibilityLabel="Code"
                onChange={({ value }) => log(`onChange: ${value.length} characters`)}
                onSelectionChange={({ selections }) => {
                  const head = selections[selections.length - 1].head;
                  caret.set(`${head.line + 1}:${head.column + 1}${selections.length > 1 ? ` (${selections.length} carets)` : ""}`);
                }}
                onSearchChange={({ result }) => found.set(result)}
                onFocus={() => log("onFocus")}
                onBlur={() => log("onBlur")}
                onLineNumberPress={({ line }) => log(`onLineNumberPress: ${line + 1}`)}
                onContextMenu={(e) => log(`onContextMenu: ${e.position.line + 1}:${e.position.column + 1}`)}
                style={{ flex: 1 }}
              />
            );
          }}
        </View>
      </Split>
    </View>
  );
}

mount(App);

Редактор отвечает за поведение и имеет собственный внешний вид, построенный на токенах темы: font-mono темы размером 13pt в строках высотой 20pt, номера строк цвета muted-foreground, едва заметная полоса muted за строками с кареткой, выделение оттенком primary, направляющие отступов цвета border и встроенные цвета синтаксиса, которые используют блоки кода TextView, в светлой и тёмной теме. Каждая часть берёт стили приложения поверх этих значений по умолчанию (classes и styles). В Rust редактор создаётся с помощью nsh_base::editor(), в TSX — с помощью <Editor> из nsh/base.

use nsh_base::editor;
use nsh_core::prelude::*;

const MAIN: &str = "fn main() {\n    println!(\"Hello, world!\");\n}\n";

fn code() -> impl IntoElement {
    let source = signal(SharedString::from(MAIN));
    editor()
        .value(move || Some(source.get()))
        .language("rust")
        .on_change(move |value: &SharedString| source.set(value.clone()))
        .flex_1()
}

Редактор заполняет область, которую ему отводит приложение (flex-1, высота), и прокручивает текст внутри неё: по вертикали всегда, по горизонтали — когда строки длиннее области, либо вообще не прокручивает при softWrap, когда длинные строки переносятся по ширине редактора.

#Текст

  • Управляемый режим: передайте value и задавайте его заново в onChange, который сообщает весь новый текст. Если приложение меняет value (загружен файл, получен результат форматтера), текст заменяется одной правкой, которую отмена возвращает целиком; каретки и свёрнутые блоки, находящиеся вдали от изменённого места, остаются на месте.
  • Неуправляемый режим: передайте defaultValue и читайте текст из onChange или из value() ref.

Переводы строк — \n: \r\n и \r во входящем тексте превращаются в \n.

#Языки

language задаёт язык по названию, псевдониму, расширению или имени файла, в любом регистре: "rust", "rs", "typescript", "tsx", "main.go", "Dockerfile", "Makefile". Он определяет подсветку, комментарий, который переключает mod+/, скобки, которые сопоставляются и задают отступ, пары, которые закрываются при наборе, символы, из которых состоят слова, и отступ по умолчанию: четыре пробела, два для веб-языков (JavaScript, TypeScript, JSON, HTML, CSS, YAML и других), табуляция для Go и Makefile. tabSize и hardTabs задают отступ для всех языков. Любое другое имя означает обычный текст, если только приложение не зарегистрировало его (собственные языки).

Языки: Rust, JavaScript, JSX, TypeScript, TSX, JSON, Python, Go, C, C++, C#, Java, Kotlin, Swift, Objective-C, Objective-C++, Ruby, PHP, Lua, Shell, Fish, PowerShell, SQL, HTML, XML, Vue, Svelte, CSS, SCSS, Sass, Less, Markdown, YAML, TOML, INI, Dockerfile, Makefile, CMake, Dart, Scala, Elixir, Erlang, Haskell, OCaml, F#, Clojure, Zig, Perl, R, Julia, Nix, Terraform, Protocol Buffers, GraphQL и Diff.

<Editor value={source()} onChange={({ value }) => source.set(value)} language={file().name} tabSize={2} softWrap class="flex-1" />

#Собственные языки

Код на Rust может научить редакторы приложения новым языкам и подсвечивать любой язык собственным подсветчиком (грамматикой tree-sitter или парсером приложения). register_language регистрирует EditorLanguage: его название, псевдонимы, расширения и имена файлов, а также правила редактирования (комментарии, скобки, закрываемые при наборе пары, символы слов, отступы); syntax указывает встроенную грамматику, которой он будет подсвечиваться. register_highlighter назначает языку, встроенному или зарегистрированному, подсветчик приложения: тип, реализующий EditorHighlighter, который получает сведения о каждой правке (строки, байты и позиции в том виде, в каком их принимают правки tree-sitter) и возвращает токены каждой строки (EditorToken, либо capture tree-sitter или scope TextMate через EditorToken::from_scope) в пределах отведённого времени, вне потока UI. Редактор хранит токены, сдвигает их вместе с текстом при правках и заново запрашивает видимые строки, как и у встроенного подсветчика. language сначала ищет зарегистрированные языки по названию, псевдониму, расширению или пути, как в редакторах, так и в представлениях diff.

У каждого движка свои языки. Приложение на Rust регистрирует их в корне до монтирования редакторов; для приложения на TSX их регистрирует крейт приложения в setup его набора UI, который первым вызывает каждый движок, запускающий приложение.

use nsh_base::editor::{EditorLanguage, register_highlighter, register_language};
use nsh_core::prelude::*;

static NIMBLE: EditorLanguage = EditorLanguage::new("Nimble")
    .extensions(&["nb"])
    .line_comment("#")
    .tab_size(2);

#[native_module("my-app/native")]
fn native(registry: &mut Registry) {
    registry.setup(|| {
        register_language(&NIMBLE);
        register_highlighter("nimble", || Box::new(NimbleHighlighter::default()));
    });
}
<Editor value={source()} onChange={({ value }) => source.set(value)} language="build.nb" class="flex-1" />

#Редактирование

Редактор работает так, как привыкли пользователи редакторов кода:

  • Несколько кареток. Alt-клик (Option-клик) добавляет каретку, mod+d выделяет следующее вхождение выделенного, mod+shift+l — все вхождения, а клавиши добавления каретки добавляют её на строку выше или ниже. Набор, удаление, перемещение и любая команда действуют у каждой каретки; Escape возвращает одну. undoSelection (cmd+u, ctrl+u) возвращает выделения, бывшие до последнего перемещения.
  • Расширение выделения по единицам с помощью expandSelection (ctrl+shift+cmd+right, shift+alt+right): слово, содержимое строки и строка целиком, содержимое пары скобок и пара целиком, строка, весь текст; shrinkSelection возвращает назад тем же путём. splitSelectionIntoLines (shift+alt+i) превращает выделение на нескольких строках в по одному выделению на строку.
  • Отступы. Новая строка сохраняет отступ текущей и добавляет уровень после открывающей скобки (: в Python, do в Ruby и т. д.); Enter между парой скобок переносит закрывающую на отдельную строку. Tab и Shift+Tab увеличивают и уменьшают отступ выбранных строк; Backspace среди начальных пробелов возвращается к предыдущей позиции отступа. autoIndent отключает это поведение.
  • Скобки и кавычки закрываются по мере набора, оборачивают выделение и пропускаются, если закрывающая набирается повторно (autoClose). Скобка у каретки и её пара обводятся рамкой.
  • Отмена возвращает целые серии набора: слово, удаление, команду. Перемещение или нажатие начинает новый шаг.
  • Сворачивание. Строка, за которой следуют строки с большим отступом, сворачивает их с маркером в жёлобе: ⌄ — свернуть, › и ⋯ после строки — развернуть. Каретка, перемещённая в свёрнутый блок, раскрывает его (Сворачивание).
  • Нажатия. С указателем: клик ставит каретку, двойной клик выделяет слово, тройной — строку, Shift-клик расширяет выделение, а перетаскивание выделяет по единице, выбранной кликом. Перетаскивание с Alt+Shift (Option+Shift; на Linux также Control+Alt) выделяет блок: по одному выделению на строку между одними и теми же столбцами, которые продолжаются и за концом коротких строк. На сенсорных экранах касание ставит каретку, двойное касание или долгое нажатие выделяет слово под пальцем, а маркеры на концах выделения перемещают каждый конец вслед за пальцем (окружающий текст при этом по-прежнему прокручивается).
  • Буфер обмена. Копирование и вырезание без выделения берут строки целиком, на которых стоят каретки, и при вставке они возвращаются как целые строки над строкой каретки.

readOnly сохраняет текст как есть, пока он остаётся выделенным, доступным для поиска и копирования.

#Клавиши

Клавиши редактора работают, пока у него есть фокус; другие поля ввода сохраняют свои. mod — это Command в macOS и iOS и Control на остальных платформах (сочетания клавиш keymap).

Клавиши (macOS)Клавиши (Windows, Linux)Команда
Стрелки, с Shift для выделенияСтрелки, с Shift для выделенияПеремещение по символам и строкам
alt+left / alt+rightctrl+left / ctrl+rightПеремещение по словам
cmd+left / cmd+right, home / end, ctrl+a / ctrl+ehome / endПервый непробельный символ строки (затем её начало) / конец строки
cmd+up / cmd+downctrl+home / ctrl+endНачало / конец текста
pageup / pagedownpageup / pagedownСтраница вверх / вниз
alt+backspace / alt+deletectrl+backspace / ctrl+deleteУдалить слово слева / справа
cmd+backspace / cmd+delete, ctrl+kctrl+shift+backspace / ctrl+shift+deleteУдалить до начала / до конца строки
cmd+z / cmd+shift+zctrl+z / ctrl+shift+z, ctrl+yОтменить / повторить
cmd+actrl+aВыделить всё
cmd+lctrl+lВыделить строку (повторно: и следующую)
cmd+dctrl+dВыделить следующее вхождение
cmd+shift+lctrl+shift+lВыделить все вхождения
cmd+alt+up / cmd+alt+downctrl+alt+up / ctrl+alt+downДобавить каретку выше / ниже
escapeescapeВернуться к одному выделению
alt+up / alt+downalt+up / alt+downПереместить строки вверх / вниз
alt+shift+up / alt+shift+downalt+shift+up / alt+shift+downДублировать строки вверх / вниз
cmd+shift+kctrl+shift+kУдалить строки
cmd+enter / cmd+shift+enterctrl+enter / ctrl+shift+enterНовая строка ниже / выше
tab / shift+tab, cmd+] / cmd+[tab / shift+tab, ctrl+] / ctrl+[Увеличить / уменьшить отступ
cmd+/ctrl+/Переключить строчный комментарий
cmd+shift+\ctrl+shift+\Перейти к парной скобке
ctrl+jctrl+jОбъединить строки
ctrl+tПоменять местами символы вокруг каретки
ctrl+alt+left / ctrl+alt+right, с Shift для выделенияПеремещение по частям слова (camelCase, snake_case)
shift+alt+ashift+alt+aПереключить блочный комментарий
shift+alt+ishift+alt+iРазбить выделение на строки
ctrl+shift+cmd+right / ctrl+shift+cmd+leftshift+alt+right / shift+alt+leftРасширить / сузить выделение
cmd+uctrl+uВернуться к предыдущим выделениям
cmd+g / cmd+shift+g, f3 / shift+f3f3 / shift+f3Найти следующее / предыдущее совпадение
cmd+eНайти выделенное (onFindWithSelection)
cmd+alt+[ / cmd+alt+]ctrl+shift+[ / ctrl+shift+]Свернуть / развернуть строку каретки
f12f12Перейти к определению у каретки (onDefinitionRequest)
cmd+.ctrl+.Действия с кодом для выделения (onCodeActionRequest)
cmd+c / cmd+x / cmd+vctrl+c / ctrl+x / ctrl+vКопировать / вырезать / вставить

В macOS и iOS также работают текстовые клавиши Cocoa: ctrl+b / ctrl+f / ctrl+p / ctrl+n перемещают, ctrl+h / ctrl+d удаляют.

Команды без клавиш по умолчанию, а также собственные команды приложения получают клавиши, которые приложение привязывает через keymap, и выполняются через run ref:

import { keymap } from "nsh";

let editor!: EditorRef;

keymap.bind({
  "f9": () => editor.run("sortLinesAscending"),
  "mod+shift+u": () => editor.run("transformToUppercase"),
  "mod+alt+0": () => editor.run("foldAll"),
  "mod+alt+j": () => editor.run("unfoldAll"),
});

#Поиск и замена

search — то, что нужно найти: { query, caseSensitive?, wholeWord?, regex?, inSelection? }. Совпадения подсвечиваются, а то, на котором стоит выделение, — сильнее; onSearchChange сообщает { result }, { count, current, error }: сколько всего совпадений, на каком из них стоит выделение (с 0) и почему регулярное выражение недопустимо. findNext и findPrevious выделяют следующее и предыдущее совпадение с переходом по кругу; replace(text) заменяет текущее совпадение и выделяет следующее, а replaceAll(text) заменяет все совпадения одной правкой и возвращает { count } — их количество. В замене регулярного выражения на его группы ссылаются как $1 и ${name}.

inSelection ищет только внутри того, что было выделено в момент включения (внутри строк кареток, если выделения не было): область сохраняется, пока режим включён, в том числе при правках, пока выделение переходит от совпадения к совпадению. findWithSelection (cmd+e в macOS) сообщает выделенный текст или слово у каретки через onFindWithSelection, чтобы панель поиска взяла его как запрос.

Панель поиска — собственная для приложения:

import { signal, Text, View } from "nsh";
import { Button, Editor, type EditorRef, type EditorSearchResult, Input } from "nsh/base";

function Find(props: { source: string; onChange: (value: string) => void }) {
  let editor!: EditorRef;
  const query = signal("");
  const replacement = signal("");
  const result = signal<EditorSearchResult>({ count: 0 });
  const status = () =>
    result().error ?? (result().current != null ? `${result().current! + 1} of ${result().count}` : `${result().count} found`);
  return (
    <View class="flex-1">
      <View class="flex-row items-center gap-2 border-b border-border p-2">
        <Input value={query()} onChange={({ value }) => query.set(value)} onSubmit={() => editor.run("findNext")} placeholder="Find" class="flex-1" />
        <Text class="text-sm text-muted-foreground">{status()}</Text>
        <Button onPress={() => editor.run("findPrevious")} class="px-2"><Text>↑</Text></Button>
        <Button onPress={() => editor.run("findNext")} class="px-2"><Text>↓</Text></Button>
        <Input value={replacement()} onChange={({ value }) => replacement.set(value)} placeholder="Replace" class="flex-1" />
        <Button onPress={() => editor.replace(replacement())} class="px-2"><Text>Replace</Text></Button>
        <Button onPress={async () => console.log("replaced", (await editor.replaceAll(replacement())).count)} class="px-2"><Text>All</Text></Button>
      </View>
      <Editor
        ref={editor}
        value={props.source}
        onChange={(e) => props.onChange(e.value)}
        language="ts"
        search={query() ? { query: query() } : undefined}
        onSearchChange={(e) => result.set(e.result)}
        class="flex-1"
      />
    </View>
  );
}

#Диагностика

diagnostics отмечает проблемы, о которых сообщает языковой сервер или линтер: тонким волнистым подчёркиванием под каждым диапазоном цветом его уровня серьёзности (error, warning, info, hint). Позиции задаются строками и столбцами UTF-16, начиная с 0, как у языкового сервера, поэтому его диагностика проходит без преобразований. Отметки остаются на своём тексте, пока пользователь печатает, до тех пор, пока приложение не передаст новые; показ сообщений (список, всплывающая карточка у строки) остаётся за приложением.

Диагностика несёт и остальные поля Diagnostic языкового сервера. Её tags отображают текст так, как это делают языковые клиенты: код unnecessary (неиспользуемая переменная, недостижимый код) блёкнет, а код deprecated зачёркивается, в частях unnecessaryText и deprecatedText; подсказка с тегом отображается только в своём тексте. codeDescription.href — страница, описывающая её код (например, правило линтера), а relatedInformation перечисляет другие места, где о ней рассказано ({ location: { uri?, range }, message }): при наведении оба значения передаются приложению, которое показывает их в своей карточке и открывает страницу. Языковой сервер присылает теги числами (1, 2); редактор принимает их имена.

const problems = signal<EditorDiagnostic[]>([]);

async function lint(text: string) {
  // [{ range, severity, message, source?, code?, codeDescription?, tags?, relatedInformation? }]
  problems.set(await check(text));
}

<Editor
  value={source()}
  onChange={({ value }) => {
    source.set(value);
    lint(value);
  }}
  language="ts"
  diagnostics={problems()}
  classes={{ diagnosticWarning: "bg-orange-500", unnecessaryText: "text-zinc-400" }}
  class="flex-1"
/>

#Декорации

decorations стилизуют диапазоны, которые выбирает приложение: семантическая подсветка от языкового сервера, выполняемый оператор, строки, изменённые при ревью. class и style декорации задают стиль её текста; lineClass и lineStyle задают стиль строк, на которых она лежит, позади текста. Как и диагностика, декорации остаются на своём тексте при правках, пока приложение не задаст новые.

const range = (line: number, start: number, end: number) => ({ start: { line, column: start }, end: { line, column: end } });

<Editor
  value={source()}
  language="sql"
  readOnly
  decorations={[
    { range: range(4, 0, 0), lineClass: "bg-green-500/10" },
    { range: range(7, 6, 18), class: "underline text-blue-600" },
  ]}
  class="flex-1"
/>

#Метки в желобе и на обзорной линейке

gutter декорации ставит метку в полосе перед номерами строк, рядом с каждой строкой, которую покрывает её диапазон: строки, добавленные или изменённые в диффе системы контроля версий, или точка останова (пустой диапазон покрывает свою строку, как и с lineClass). С placement: "before" это линия поперёк полосы на границе перед началом диапазона, где строки были удалены. Ширина полосы — gutterDecorationsWidth пунктов (по умолчанию 3), и она остаётся, пока у декорации есть метка в желобе, поэтому текст не сдвигается, когда метки появляются и исчезают. Диапазон внутри свёрнутого блока отмечает строку этого блока.

overviewRuler декорации ставит метку на обзорной линейке — полосе вдоль правого края редактора шириной 14 пунктов (как у полосы прокрутки; другую ширину задаёт часть overviewRuler), которая, как в Monaco, отображает весь текст на свою высоту. Метка имеет высоту не менее 6 пунктов и занимает lane линейки: left, center (по умолчанию), right или full. Линейка показывается, пока у декорации есть на ней метка.

Каждая метка берёт class и style приложения, а без них рисуется цветом переднего плана. Обе следуют за правками так же, как диапазон декорации; правка, удаляющая отмеченный диапазон, оставляет на его месте пустой.

<Editor
  value={source()}
  onChange={({ value }) => source.set(value)}
  language="ts"
  decorations={[
    { range: range(2, 0, 0), gutter: { class: "bg-yellow-500" }, overviewRuler: { lane: "left", class: "bg-yellow-500" } },
    { range: { start: { line: 3, column: 0 }, end: { line: 5, column: 0 } }, gutter: { class: "bg-green-500" } },
    { range: range(6, 0, 0), gutter: { class: "bg-red-500", placement: "before" }, overviewRuler: { lane: "left", class: "bg-red-500" } },
  ]}
  class="flex-1"
/>

#Собственная подсветка

token декорации окрашивает её текст так, как встроенная подсветка окрашивает токен данного вида (comment, keyword, string, number, title, attr, built, deletion), вместо того, что нашёл там встроенный подсветчик: собственный подсветчик приложения (семантические токены языкового сервера, собственный парсер) сопоставляет свои токены с этими видами и получает цвета редактора для светлой и тёмной тем. Побеждает токен самой вложенной декорации; class и style применяются поверх него. highlighting={false} отключает встроенный подсветчик, так что текст окрашивают только токены приложения, а language по-прежнему определяет комментарии, скобки и отступы.

const kinds: Record<string, EditorToken> = { keyword: "keyword", function: "title", string: "string", type: "built" };

<Editor
  value={source()}
  onChange={({ value }) => source.set(value)}
  language="ts"
  highlighting={false}
  decorations={semanticTokens().map((t) => ({ range: range(t.line, t.start, t.end), token: kinds[t.type] }))}
  class="flex-1"
/>

#Сворачивание

Блоки сворачивания начинаются там, где строки сдвинуты глубже предыдущей. foldRanges даёт редактору собственные диапазоны приложения ({ startLine, endLine } — форма диапазонов сворачивания языкового сервера, которые передаются как есть): строка startLine показывает блок, а строки после неё до endLine сворачиваются. Они остаются на своих строках при правках, пока приложение не передаст новые.

Блоки вкладываются: при сворачивании блока вложенные в него остаются свёрнутыми, так что при разворачивании блока они выглядят как прежде. foldRecursive сворачивает блок и все блоки внутри него, unfoldRecursive разворачивает их все, foldLevel(n) сворачивает все блоки на уровне вложенности n, а foldSelection (или foldRange(range)) сворачивает любые строки, где бы ни начинались блоки. onFoldChange сообщает о свёрнутых диапазонах после каждого изменения, а folds() / setFolds(ranges) читают и восстанавливают их, чтобы хранить вместе с файлом.

const ranges = signal<EditorFoldRange[]>([]);
const folded = new Map<string, EditorFoldRange[]>();

<Editor
  ref={editor}
  value={file().text}
  language={file().name}
  foldRanges={ranges()} // from the language server's textDocument/foldingRange
  onFoldChange={({ folds }) => folded.set(file().name, folds)}
  class="flex-1"
/>
<Button onPress={() => editor.foldLevel(2)}><Text>Fold the methods</Text></Button>

#Точки останова и контекстные меню

Нажатие на номер строки сообщает onLineNumberPress({ line }) — место для точки останова, которую приложение рисует как декорацию. Вторичная кнопка (правый щелчок, Control-щелчок на Mac) сообщает onContextMenu({ position, x, y }): позицию в тексте под указателем и точку в корневом представлении, где приложение открывает собственное меню. На сенсорных экранах то же сообщают долгое нажатие, двойное касание, отпущенный маркер и касание выделения, с selection: рамка выделения в корневом представлении ({ x, y, width, height }), а также x / y в её верхней середине — для меню правки.

<Editor
  value={source()}
  onContextMenu={({ x, y, selection }) =>
    Menu.popup(selection ? editItems : contextItems, { x, y })
  }
/>
const breakpoints = signal<number[]>([]);

<Editor
  value={source()}
  language="py"
  onLineNumberPress={({ line }) =>
    breakpoints.set(breakpoints().includes(line) ? breakpoints().filter((l) => l !== line) : [...breakpoints(), line])
  }
  decorations={breakpoints().map((line) => ({ range: range(line, 0, 0), lineClass: "bg-red-500/15" }))}
  class="flex-1"
/>

#Языковые функции

Подсказки, информация при наведении, переход к определению и действия с кодом приходят от языкового сервера приложения. Редактор сам рисует список автодополнения, встроенное автодополнение и ссылку на определение, со своими клавишами; для остального он сообщает приложению моменты и места, а приложение рисует свои всплывающие окна (Overlay, привязанный к pageBounds).

#Автодополнение

С completion редактор запрашивает у приложения варианты и показывает их списком под набираемым словом (над ним — у нижнего края окна):

  • Ввод буквы, цифры или _ в конце слова запрашивает варианты через onCompletionRequest(event), триггер "typing": у event есть id, offset и position курсора, query (набранная часть слова) и смещение queryStart, с которого оно начинается. Каждый из completionTriggerCharacters (["."], ["::", "<"]) после себя запрашивает варианты с пустым запросом, триггер "character" (и character), а showCompletions() или run("showCompletions") запрашивает варианты у курсора, триггер "invoked".
  • Приложение отвечает через presentCompletions(event.id, items). Каждый элемент имеет форму CompletionItem языкового сервера: label и, по желанию, kind ("function", "variable", "class", …), detail, documentation, insertText, filterText, sortText и range. Показывается ответ на последний запрос; более ранний, а также ответ для слова, которое курсор уже покинул, отбрасывается.
  • Дальнейший ввод фильтрует список на месте: запрос сопоставляется с filterText каждого элемента (иначе с его label) как буквы по порядку, без учёта регистра; сначала идут слова, начинающиеся с запроса, затем ближайшие совпадения, затем порядок по sortText. Совпавшие буквы выделяются. Передайте incomplete: true (presentCompletions(id, items, true)), чтобы редактор запрашивал варианты заново по мере ввода.
  • Вверх и Вниз (а также Control+P / Control+N) перемещаются по списку, Page Up и Page Down — на страницу, Enter и Tab принимают вариант, Escape закрывает список. Указатель отмечает элемент, а щелчок (касание на сенсорных экранах) принимает его. documentation выбранного элемента показывается рядом со списком.
  • Принятие заменяет range элемента (иначе запрос) его insertText (иначе label) одним шагом, который отменяется undo, у курсора и у каждого другого курсора с тем же текстом перед ним, и сообщает onCompletionAccept({ item }).
  • Список закрывается, когда курсор покидает слово, при пробеле или другой пунктуации, когда редактор теряет фокус и когда курсор прокручивается за пределы видимой области.

Пауза в наборе (inlineCompletionDelay, по умолчанию 300 мс) при отсутствии списка запрашивает встроенное автодополнение: onInlineCompletionRequest({ id, offset, position }). presentInlineCompletion(id, text) показывает text у курсора приглушённым цветом, Tab вставляет его, а Escape, правка или перемещение убирают.

let editor!: EditorRef;

<Editor
  ref={(r) => (editor = r)}
  value={source()}
  onChange={({ value }) => source.set(value)}
  language="ts"
  completion
  completionTriggerCharacters={["."]}
  onCompletionRequest={async (e) => {
    const items = await server.completion(e.position);
    editor.presentCompletions(e.id, items);
  }}
  onInlineCompletionRequest={async (e) => {
    editor.presentInlineCompletion(e.id, await assistant.suggest(e.offset));
  }}
  class="flex-1"
/>

#Наведения и нажатия

  • onHover(event): мышь остановилась над символом, ни одна кнопка не нажата (десктопы и веб; перетаскивание выделяет текст). У event есть position, его UTF-16 offset, range его слова, pagePosition указателя, pageBounds слова, зажатые modifiers и diagnostics в этом месте, сообщения которых показывает приложение. Переход к другому слову сразу сообщает о следующем наведении.
  • onHoverEnd(): мышь покинула слово или редактор, либо текст изменился или прокрутился под ней.
  • onTextPress(event): символ был нажат после того, как редактор поставил в него курсор: { position, offset, range, pagePosition, modifiers, clicks }.
  • caretBounds() и rangeBounds(range): где находятся курсор и диапазон, { bounds, pageBounds } (в редакторе и в корневом представлении): там открывается всплывающее окно приложения.
const hover = signal<EditorHoverEvent | null>(null);

<Editor
  ref={editor}
  value={source()}
  language="ts"
  onHover={(event) => hover.set(event)}
  onHoverEnd={() => hover.set(null)}
  class="flex-1"
/>
<If when={hover()}>
  {(event) => (
    <Overlay anchor={event().pageBounds} placement="top" class="rounded-md border border-border bg-popover p-2">
      <Text>{event().diagnostics.map((d) => d.message).join("\n") || describe(event().range)}</Text>
    </Overlay>
  )}
</If>

#Переход к определению и действия с кодом

Удерживаемая клавиша Mod (Command на macOS и iOS, Control на остальных платформах) превращает слова под мышью в ссылки, как в редакторах кода:

  • Когда Mod удерживается над словом (нажата, пока мышь остановилась там, или мышь перешла туда с уже удерживаемой клавишей), редактор запрашивает через onDefinitionRequest(event), триггер "hover": у event есть id, position и offset символа и range слова. Отвечайте через presentDefinitionLink(event.id, range), когда языковой сервер находит определение: диапазон подчёркивается (часть definitionLink), а указатель над ним становится рукой. Ссылка исчезает, когда Mod отпущена, мышь покидает её, текст меняется или прокручивается, либо по hideDefinitionLink(). Показывается ответ на последний запрос; более ранний отбрасывается.
  • Щелчок с Mod по слову запрашивает определение с триггером "press"; при щелчке по ссылке курсор остаётся на месте. F12 (goToDefinition, run("goToDefinition")) запрашивает определение для слова у курсора, триггер "key". Переход к определению выполняет приложение: revealPosition и setSelections для определения в том же тексте, собственная навигация — для другого файла.
  • Mod+. (showCodeActions, run("showCodeActions")) сообщает onCodeActionRequest({ range, position, bounds }): основное выделение (начало первым), курсор и его рамку. Приложение запрашивает у языкового сервера действия, показывает своё меню в bounds.pageBounds (Menu или Overlay) и применяет правки выбранного действия через edit(edits).
let editor!: EditorRef;

<Editor
  ref={(r) => (editor = r)}
  value={source()}
  onChange={({ value }) => source.set(value)}
  language="ts"
  onDefinitionRequest={async (e) => {
    const target = await server.definition(e.position);
    if (!target) return;
    if (e.trigger === "hover") editor.presentDefinitionLink(e.id, target.originRange ?? e.range);
    else editor.revealPosition(target.range.start, true);
  }}
  onCodeActionRequest={async (e) => {
    const actions = await server.codeActions(e.range);
    const picked = await choose(actions, e.bounds?.pageBounds);
    if (picked) editor.edit(picked.edits);
  }}
  class="flex-1"
/>

#Размер и прокрутка

С minRows и / или maxRows редактор растёт вместе с текстом: его высота равна высоте его строк, не менее minRows из них, а начиная с maxRows текст прокручивается, как в окне сообщения или ячейке блокнота. В остальных случаях размер задаёт приложение, а scrollBeyondLastLine позволяет тексту прокручиваться дальше последней строки: до экрана (true, по умолчанию), совсем нет (false) или на заданное число строк. При перемещении курсора вокруг него в поле зрения остаётся cursorSurroundingLines строк. Перетаскивание выделения за края редактора продолжает прокрутку, тем быстрее, чем дальше уходит указатель.

showWhitespaces показывает · для каждого пробела и → для каждой табуляции (часть whitespace), а rulers рисует линию в каждом из своих столбцов (часть ruler) как ориентиры длины строки.

<Editor
  defaultValue={message()}
  language="markdown"
  minRows={2}
  maxRows={8}
  rulers={[72]}
  showWhitespaces
/>

#Методы

ref={editor} даёт методы редактора (EditorRef) для кнопок, меню и клавиш приложения. Методы, возвращающие значение (value(), selections(), replaceAll(), canUndo(), caretBounds(), folds(), …), в JS возвращают Promise с ним, поэтому используйте await: const text = await editor.value(). Метод, которому передана строка за пределами текста (fold, scrollToLine, goToLine, …), ничего не меняет и пишет в журнал предупреждение с именем метода и строкой.

МетодОписание
focus(), blur()Берёт фокус или отдаёт его.
value()Promise<string>: текст.
setValue(value)Заменяет текст как правку, которую отменяет undo.
insert(text)Вводит text в каждое выделение как есть (скобки не закрываются).
edit(edits)Применяет замены { range, text } (диапазоны текущего текста; замена, пересекающаяся с более ранней, пропускается) одним шагом, который отменяет undo; выделения смещаются вместе с окружающим текстом. Сюда идут правки действий с кодом и форматирования.
selections()Promise<EditorSelection[]>: каждое выделение, { anchor, head }, основное (новейшее) последним.
setSelections(selections)Задаёт выделения (последнее — основное) и прокручивает основное в область видимости.
run(command)Выполняет команду, как это сделали бы её клавиши (Commands).
replace(text)Заменяет текущее совпадение и выделяет следующее.
replaceAll(text)Promise<EditorReplaceAll>: заменяет все совпадения одной правкой; { count } — сколько их.
fold(line), unfold(line)Сворачивает строки под строкой line (с 0) или разворачивает их.
foldRecursive(line), unfoldRecursive(line)Сворачивает блок на строке line и каждый блок внутри него или разворачивает их все.
foldLevel(level)Сворачивает каждый блок на уровне вложенности level (1: самый внешний).
foldRange(range)Сворачивает строки range ({ startLine, endLine }), где бы ни начинались блоки.
folds()Promise<EditorFoldRange[]>: свёрнутые диапазоны, включая вложенные.
setFolds(folds)Сворачивает ровно folds.
scrollToLine(line)Прокручивает строку line в область видимости, разворачивая то, что её скрывает.
canUndo(), canRedo()Promise<boolean>: есть ли правка, которую можно отменить или повторить.
caretBounds()Promise<EditorBounds | null>: рамка основного курсора, { bounds, pageBounds }, в редакторе (с учётом прокрутки) и в корневом представлении; null до компоновки редактора.
rangeBounds(range)Promise<EditorBounds[]>: рамки range, по одной на каждую покрываемую строку.
scrollOffset(), setScrollOffset(offset)Promise<Point>: насколько прокручен текст; прокручивает его до { x, y }.
visibleRange()Promise<EditorRange>: текст, который показывает область просмотра, от начала её первой строки до конца последней.
selectedText()Promise<string>: текст основного выделения.
offsetToPosition(offset), positionToOffset(position)Promise<EditorPosition>, Promise<number>: между UTF-16-смещениями value() и позициями.
revealPosition(position, center)Прокручивает position в область видимости, разворачивая то, что её скрывает; в середину — с center.
goToLine(line)Ставит курсор в начало строки line (с 0) и прокручивает её в середину.
showCompletions()Запрашивает варианты автодополнения у курсора, запросом служит слово перед ним (Completion).
presentCompletions(id, items, incomplete?)Отвечает на запрос автодополнения id: список показывает items, отфильтрованные по набранному с тех пор; с incomplete дальнейший ввод запрашивает варианты заново. Без элементов список закрывается.
hideCompletions()Закрывает список автодополнения.
presentInlineCompletion(id, text), hideInlineCompletion()Отвечает на запрос встроенного автодополнения id через text, показываемое у курсора; убирает его.
presentDefinitionLink(id, range), hideDefinitionLink()Отвечает на запрос определения id (триггер "hover"): range подчёркивается как ссылка (Go to definition); скрывает её.
let editor!: EditorRef;

<Button onPress={() => editor.run("toggleComment")}><Text>Comment</Text></Button>
<Button onPress={async () => save(await editor.value())}><Text>Save</Text></Button>
<Button onPress={async () => (await editor.canUndo()) && editor.run("undo")}><Text>Undo</Text></Button>
<Button onPress={() => editor.setSelections([{ anchor: { line: 0, column: 0 }, head: { line: 0, column: 0 } }])}>
  <Text>Go to the top</Text>
</Button>
<Editor ref={editor} value={source()} onChange={({ value }) => source.set(value)} language="rs" class="flex-1" />

#Команды

run принимает команды редактора по имени (EditorCommand):

КомандыЧто делают
moveLeft, moveRight, moveUp, moveDown, moveWordLeft, moveWordRight, moveLineStart, moveLineEnd, movePageUp, movePageDown, moveDocumentStart, moveDocumentEndПеремещают каретки.
selectLeft, selectRight, selectUp, selectDown, selectWordLeft, selectWordRight, selectLineStart, selectLineEnd, selectPageUp, selectPageDown, selectDocumentStart, selectDocumentEndРасширяют выделения.
moveToMatchingBracketК скобке, парной той, что находится у каретки.
selectAll, selectLine, selectWord, selectNextOccurrence, selectPreviousOccurrence, selectAllOccurrencesВыделяют.
moveSubwordLeft, moveSubwordRight, selectSubwordLeft, selectSubwordRightПеремещают или расширяют выделение на часть слова («горбы» camelCase, части snake_case).
moveParagraphUp, moveParagraphDown, selectParagraphUp, selectParagraphDownПеремещают или расширяют до пустой строки перед абзацем или после него.
expandSelection, shrinkSelectionУвеличивают выделения на одну единицу или возвращают назад.
undoSelection, redoSelectionВозврат к выделениям, бывшим до последнего перемещения, или снова вперёд.
splitSelectionIntoLinesОдно выделение на каждую строку каждого выделения.
addCaretAbove, addCaretBelow, singleSelectionДобавляют каретку; возврат к одному выделению.
deleteBackward, deleteForward, deleteWordLeft, deleteWordRight, deleteToLineStart, deleteToLineEnd, deleteLineУдаляют.
newline, newlineBelow, newlineAbove, indent, outdent, toggleComment, toggleBlockCommentСтроки, отступы и комментарии.
sortLinesAscending, sortLinesDescendingСортируют выделенные строки (все строки, если есть только каретка).
transformToUppercase, transformToLowercase, transformToTitleCaseМеняют регистр выделений (для каретки — её слова).
trimTrailingWhitespaceУдаляет пробельные символы в конце каждой строки.
moveLinesUp, moveLinesDown, duplicateLinesUp, duplicateLinesDown, joinLines, transposeCharactersПереставляют.
undo, redoОтмена и повтор.
fold, unfold, foldAll, unfoldAllСворачивают блок на строке каретки или всё.
foldRecursive, unfoldRecursiveСворачивают блок каретки и все вложенные в него блоки или разворачивают их.
foldSelectionСворачивают строки каждого выделения, охватывающего несколько строк.
findNext, findPreviousВыделяют следующее или предыдущее совпадение search.
findWithSelectionСообщает выделенный текст для панели поиска (onFindWithSelection).
copy, cut, pasteБуфер обмена.
showCompletionsЗапрашивает варианты автодополнения у каретки (Автодополнение).
goToDefinitionЗапрашивает определение слова у каретки (onDefinitionRequest, триггер "key").
showCodeActionsЗапрашивает действия с кодом для выделения (onCodeActionRequest).

#Стилизация частей

class и style стилизуют блок редактора. classes принимает классы Tailwind по частям, а styles — встроенные стили по частям; классы части идут после её собственных стилей по умолчанию, поэтому имеют приоритет, а её styles имеют приоритет над её классами. Шрифт задаётся через fontFamily, fontSize и lineHeight; в нём редактор измеряет свой текст.

<Editor
  value={source()}
  onChange={({ value }) => source.set(value)}
  language="tsx"
  fontFamily="JetBrains Mono"
  fontSize={14}
  lineHeight={22}
  class="flex-1 rounded-lg border border-border"
  classes={{
    gutter: "bg-muted",
    lineNumber: "text-xs",
    activeLine: "bg-yellow-500/10",
    selection: "bg-blue-500/30",
  }}
  styles={{ caret: { backgroundColor: "#f59e0b", width: 3 } }}
/>
ЧастьЧто этоПо умолчанию
gutterКолонка номеров строк и маркеров сворачивания12pt перед номерами, 4pt после маркеров
lineNumberКаждый номер строкиПо правому краю, muted-foreground
activeLineNumberНомера строк с кареткой, после lineNumberforeground
foldMarkerМаркер строки, где начинается блок сворачивания⌄, › в свёрнутом виде, muted-foreground
overviewRulerОбзорная линейка вдоль правого края, пока там есть метка декорациинет
contentОбласть текста рядом с боковым полем4pt перед текстом
lineКаждая строка текста
activeLineПод строками с кареткой, ниже индикаторов прокруткиmuted, бледный
selectionПод выделенным текстомОттенок primary
caretКареткиШириной 2pt, foreground, мигают
compositionТекст, который набирается через метод вводаПодчёркнут
searchMatchСовпадения searchПолупрозрачный янтарный
currentSearchMatchСовпадение, на котором стоит выделение, после searchMatchБолее насыщенный янтарный
matchingBracketСкобка у каретки и парная ейРамка 1pt
indentGuideНаправляющая на каждый уровень отступаЛиния 1pt цвета border
foldPlaceholderЧто свёрнутая строка показывает после своего текста⋯, muted-foreground
placeholderПоказывается, пока текст пустmuted-foreground
diagnosticErrorЛиния под ошибкойdestructive
diagnosticWarningЛиния под предупреждениемЯнтарная
diagnosticInfoЛиния под информационным сообщениемСиняя
diagnosticHintЛиния под подсказкойmuted-foreground
unnecessaryTextТекст диагностики unnecessarymuted-foreground
deprecatedTextТекст диагностики deprecatedЗачёркнут
whitespaceМетки пробелов и табуляций (showWhitespaces)· и →, muted-foreground при 60 %
rulerЛинии rulersЛиния 1pt цвета border
selectionHandleРучки маркеров на концах выделения при касанииТочка 10pt цвета primary
selectionHandleStart, selectionHandleEndРучка начала (над его строкой) и ручка конца (под ней), после selectionHandle
completionMenuБлок списка автодополненияpopover, border 1pt, скруглённые углы, тень
completionItemКаждый элемент спискаВысота строки, отступ 8pt
selectedCompletionItemВыбранный элемент, после completionItemaccent
completionMatchБуквы метки, совпавшие с запросомПолужирный, primary
completionDetailДетали элемента, после его меткиПо правому краю, muted-foreground
completionKindВид элемента, перед его меткойКороткое имя (fn, var, cls), muted-foreground
completionDocumentationДокументация выбранного элемента, рядом со спискомКак completionMenu, шириной не более 320pt
inlineCompletionВстроенное автодополнение у кареткиmuted-foreground
definitionLinkПодчёркивание ссылки на определениеЛиния 1pt цвета primary под текстом

#Свойства

СвойствоRustТипПо умолчаниюОписание
value.value(..)stringТекст, управляемый приложением (задайте его снова из onChange).
defaultValue.default_value(..)stringНачальный текст, когда редактор хранит текст сам.
language.language(..)stringобычный текстНазвание языка, псевдоним, расширение или имя файла (Языки).
placeholder.placeholder(..)stringПоказывается, пока текст пуст.
readOnly.read_only(..)booleanfalseТекст можно выделять и копировать, но не менять.
autoFocus.auto_focus(..)booleanfalseПолучает фокус при появлении.
lineNumbers.line_numbers(..)booleantrueНомера строк в боковом поле.
softWrap.soft_wrap(..)booleanfalseДлинные строки переносятся по ширине редактора.
tabSize.tab_size(..)numberпо языкуЧисло колонок в табуляции и в уровне отступа.
hardTabs.hard_tabs(..)booleanпо языкуОтступы делаются табуляциями.
folding.folding(..)booleantrueСтроки с отступом сворачиваются маркерами бокового поля.
foldRanges.fold_ranges(..)EditorFoldRange[]по отступамГде начинаются блоки сворачивания, задаёт приложение (Сворачивание).
highlighting.highlighting(..)booleantrueВстроенная подсветка синтаксиса (Собственная подсветка).
autoClose.auto_close(..)booleantrueСкобки и кавычки закрываются при вводе.
autoIndent.auto_indent(..)booleantrueНовая строка сохраняет отступ и добавляет отступ после открывающей скобки.
activeLine.active_line(..)booleantrueСтроки с кареткой подсвечиваются.
indentGuides.indent_guides(..)booleantrueНаправляющие отступов.
scrollBeyondLastLine.scroll_beyond_last_line(..)boolean | numbertrueТекст прокручивается за последнюю строку: до одного экрана (true), вообще не прокручивается (false) или на указанное число строк.
fontFamily.font_family(..)stringfont-mono темыШрифт, которым текст рисуется и измеряется.
fontSize.font_size(..)number13Размер шрифта в пунктах.
lineHeight.line_height(..)number20Высота строки в пунктах.
search.search(..)EditorSearchЧто искать (Поиск и замена).
diagnostics.diagnostics(..)EditorDiagnostic[][]Проблемы для пометки (Диагностика).
decorations.decorations(..)EditorDecoration[][]Диапазоны, которые стилизует приложение (Декорации).
gutterDecorationsWidth.gutter_decorations_width(..)number3Ширина полосы меток декораций в боковом поле, в пунктах.
classes.classes(..)EditorClasses{}Классы Tailwind по частям.
styles.styles(..)EditorStyles{}Встроенные стили по частям, поверх классов.
accessibilityLabel.accessibility_label(..)stringКак скринридеры называют редактор.
showWhitespaces.show_whitespaces(..)booleanfalseПробелы и табуляции отображаются как · и →.
rulers.rulers(..)number[][]Колонки, у которых рисуется вертикальная линия.
minRows.min_rows(..)numberРедактор растёт вместе с текстом, начиная с этого числа строк (Размер и прокрутка).
maxRows.max_rows(..)numberРедактор растёт вместе с текстом до этого числа строк, затем прокручивается.
cursorSurroundingLines.cursor_surrounding_lines(..)number1Число строк, остающихся в поле зрения выше и ниже каретки.
completion.completion(..)booleanfalseРедактор запрашивает подсказки и показывает их списком (Автодополнение).
completionTriggerCharacters.completion_trigger_characters(..)string[][]Что ещё запрашивает автодополнение при вводе.
inlineCompletionDelay.inline_completion_delay(..)number300Пауза в наборе в миллисекундах, после которой запрашивается встроенное автодополнение.
class, styleметоды стиляstring, StyleProp<ViewStyle>Блок редактора.

#События

СобытиеRustОписание
onChange.on_change(|value: &SharedString| ..)Текст изменился: { value } — весь новый текст.
onSelectionChange.on_selection_change(|selections: &Vec<EditorSelection>| ..)Выделения изменились: { selections } — все, основное последним.
onFocus, onBlur.on_focus(|| ..), .on_blur(|| ..)Редактор получил или потерял фокус.
onSearchChange.on_search_change(|result: &EditorSearchResult| ..)Совпадения поиска изменились: { result } — { count, current, error }.
onLineNumberPress.on_line_number_press(|line: &u32| ..)Нажат номер строки: { line } — строка, отсчёт с 0.
onContextMenu.on_context_menu(|event: &EditorContextMenuEvent| ..)Нажата дополнительная кнопка над текстом или касанием выделен текст: { position, x, y, selection? }.
onHover.on_hover(|event: &EditorHoverEvent| ..)Указатель мыши остановился над символом (Возможности языка).
onHoverEnd.on_hover_end(|| ..)Наведение закончилось.
onTextPress.on_text_press(|event: &EditorTextPressEvent| ..)Нажат символ: передаются его слово, модификаторы и число кликов.
onFindWithSelection.on_find_with_selection(|query: &String| ..)Выполнена команда findWithSelection: { query } — текст для запроса панели поиска.
onFoldChange.on_fold_change(|folds: &Vec<EditorFoldRange>| ..)Свёрнутые диапазоны изменились: { folds } — все, включая вложенные.
onCompletionRequest.on_completion_request(|event: &EditorCompletionRequest| ..)Редактор запрашивает автодополнение: { id, offset, position, query, queryStart, trigger, character? } (Автодополнение).
onCompletionAccept.on_completion_accept(|event: &EditorCompletionAcceptEvent| ..)Элемент принят и вставлен: { item }.
onInlineCompletionRequest.on_inline_completion_request(|event: &EditorInlineCompletionRequest| ..)Набор приостановлен: { id, offset, position }, для встроенного автодополнения.
onDefinitionRequest.on_definition_request(|event: &EditorDefinitionRequest| ..)Редактор запрашивает определение: { id, position, offset, range, trigger } (Переход к определению).
onCodeActionRequest.on_code_action_request(|event: &EditorCodeActionRequest| ..)Пользователь запросил действия с кодом: { range, position, bounds? }.

#Доступность

Скринридеры называют редактор по его accessibilityLabel; его фокус устроен так же, как у текстового поля ввода, поэтому текстовые службы платформы (методы ввода, диктовка, экранные клавиатуры) работают в нём.

#См. также

  • TextView: документы Markdown и HTML, блоки кода в которых используют те же цвета синтаксиса, что и редактор.
  • DiffView: изменения между двумя текстами в цветах синтаксиса редактора.
  • Input: однострочные и многострочные текстовые поля со штатным редактированием платформы.
  • keymap: клавиши приложения для команд редактора.
  • nsh/base: остальные базовые компоненты.