تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

المحرر (Editor)

Editor محرر شيفرة برمجية يوفّر: تلوين الصياغة لأكثر من خمسين لغة، وأرقام الأسطر، وعدة مؤشرات إدراج، والطي، والالتفاف التلقائي للأسطر، والبحث والاستبدال، والتشخيصات والزخارف، مع اختصارات محررات الشيفرة (نقل الأسطر ونسخها، والتعليقات، والمسافات البادئة، والأقواس وعلامات الاقتباس التي تُغلق أثناء الكتابة). ويتعامل مع ملفات بأي طول: إذ تُنشأ الصفوف الظاهرة فقط، ويتابع التلوين كل تعديل سطرًا بسطر، فيتمرّر الملف الطويل ويُكتب فيه كما يحدث في الملف القصير. تمر الكتابة عبر مدخلات المنصة، لذا تعمل طرق الإدخال (pinyin وkana والمفاتيح الميتة) ولوحات المفاتيح اللمسية كما تعمل في أي حقل نصي.

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() الخاص بالمرجع.

فواصل الأسطر هي \n: فكل \r\n و\r في نص وارد تصبح \n.

#اللغات

يسمّي language اللغة باسمها أو باسم مستعار أو بامتداد أو باسم ملف، بأي حالة أحرف: "rust" و"rs" و"typescript" و"tsx" و"main.go" و"Dockerfile" و"Makefile". وهو يحدّد التلوين، والتعليق الذي يبدّله mod+/، والأقواس التي تتطابق وتحدّد المسافة البادئة، والأزواج التي تغلقها الكتابة، والأحرف التي تتكوّن منها الكلمات، والمسافة البادئة الافتراضية: أربع مسافات، ومسافتان للغات الويب (JavaScript وTypeScript وJSON وHTML وCSS وYAML، …)، وعلامات جدولة (tabs) للغة 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، أو التقاط tree-sitter أو نطاق TextMate عبر EditorToken::from_scope) ضمن ميزانية زمنية، بعيدًا عن خيط واجهة المستخدم. يحتفظ المحرر بالرموز، ويحرّكها مع النص عند تعديله، ويطلبها من جديد للأسطر الظاهرة، كما يفعل مع الملوّن المضمّن. يجد language اللغات المسجّلة أولًا، بالاسم أو الاسم المستعار أو الامتداد أو المسار، في المحررات وعروض الفروق على حد سواء.

يحتفظ كل محرك بلغاته الخاصة. يسجّلها تطبيق Rust في جذره قبل أن يركّب محرراته؛ وتسجّلها حزمة (crate) التطبيق لتطبيق TSX في إعداد مجموعة واجهته، الذي يستدعيه أولًا كل محرك يشغّل التطبيق.

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؛ وControl+Alt أيضًا على Linux) يحدّد كتلة: تحديدًا لكل صف، بين العمودين نفسيهما، ويستمر بعد نهاية الأسطر القصيرة. على شاشات اللمس، اللمسة تضع المؤشر، واللمسة المزدوجة أو الضغط المطوّل يحدّد الكلمة تحت الإصبع، وتتحرك المقابض عند طرفي ذلك التحديد مع سحب الإصبع لها (ويستمر النص حولها في التمرير).
  • الحافظة. النسخ والقص دون تحديد يأخذان الأسطر الكاملة التي عليها المؤشرات، وتُلصق هذه كأسطر كاملة فوق سطر المؤشر.

يُبقي 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 الخاص بالمرجع:

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 المشكلات التي يبلّغ عنها خادم اللغة أو أداة فحص الشيفرة (linter): خط تحتي رفيع متموّج تحت كل نطاق بلون درجة خطورته (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 في أي تزيين (decoration) علامةً في شريط يسبق أرقام الأسطر، بجانب كل سطر يغطيه نطاقه: الأسطر التي أضافها فرق نسخ الإصدارات أو عدّلها، أو نقطة توقف (النطاق الفارغ يغطي سطره، كما في 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 كل عنصر (أو تسميته إن لم يوجد) حروفًا بالترتيب، بأي حالة أحرف، وتأتي أولًا الكلمات التي تبدأ بالاستعلام، ثم الأقرب تطابقًا، ثم الترتيب حسب sortText. وتبرز الحروف المطابقة. مرّر incomplete: true (presentCompletions(id, items, true)) ليطلب المحرر من جديد مع متابعة الكتابة.
  • السهمان للأعلى والأسفل (وControl+P / Control+N) يتنقلان في القائمة، وPage Up وPage Down بمقدار صفحة، وEnter وTab تقبلان، وEscape تغلق. يعلّم المؤشر عنصرًا، والنقر (أو اللمس على شاشات اللمس) يقبله. تظهر documentation العنصر المحدد بجانب القائمة.
  • يستبدل القبول range العنصر (أو الاستعلام) بـinsertText (أو تسميته)، في خطوة واحدة يتراجع عنها التراجع، عند مؤشر الكتابة وعند كل مؤشر آخر له النص نفسه قبله، ويبلغ عن 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، وoffset بوحدات UTF-16، و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()، …) تعيد Promise لها في JS، فاستخدم await معها: const text = await editor.value(). الدالة التي تُعطى سطرًا بعد نهاية النص (fold، scrollToLine، goToLine، …) لا تغيّر شيئًا وتسجل تحذيرًا يسمي الدالة والسطر.

الدالةالوصف
focus()، blur()يأخذ التركيز، أو يتخلى عنه.
value()Promise<string>: النص.
setValue(value)يستبدل النص، كتعديل يتراجع عنه التراجع.
insert(text)يكتب text عند كل تحديد كما هو (لا تُغلق الأقواس).
edit(edits)يطبّق استبدالات { range, text } (نطاقات من النص الحالي؛ ويُتخطى ما يتداخل مع سابق) كخطوة واحدة يتراجع عنها التراجع؛ وتتحرك التحديدات مع النص حولها. تُمرَّر هنا إجراءات الشيفرة وتعديلات التنسيق.
selections()Promise<EditorSelection[]>: كل تحديد، { anchor, head }، والأساسي (الأحدث) أخيرًا.
setSelections(selections)يضبط التحديدات (الأخير أساسي) ويمرر التحديد الأساسي إلى العرض.
run(command)يشغّل أمرًا، كما تفعل مفاتيحه (الأوامر).
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()يطلب الإكمال عند مؤشر الكتابة، والكلمة قبله هي الاستعلام (الإكمال).
presentCompletions(id, items, incomplete?)يجيب عن طلب الإكمال id: تعرض القائمة items، مصفّاة بما كُتب منذئذ؛ ومع incomplete، يطلب المحرر من جديد مع متابعة الكتابة. وإذا لم تكن هناك عناصر أُغلقت القائمة.
hideCompletions()يغلق قائمة الإكمال.
presentInlineCompletion(id, text)، hideInlineCompletion()يجيب عن طلب الإكمال المضمَّن id بـtext، يظهر عند مؤشر الكتابة؛ يمسحه.
presentDefinitionLink(id, range)، hideDefinitionLink()يجيب عن طلب التعريف id (المشغّل "hover"): يوضع خط تحت range كرابط (الانتقال إلى التعريف)؛ يخفيه.
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خط بسماكة 1 point بلون 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: المكونات الأساسية الأخرى.