از شخصیت ها به خطوط: اصلاح ویرایشگر کد من برای عملکرد بهتر و پیمایش دو طرفه

در این مقاله ، من شما را طی می کنم که چگونه ویرایشگر کد من به یک ابزار قوی تر و کارآمد تر تبدیل شد. در مقاله قبلی ، من سعی کردم ورودی کاربر و ارائه متن را در سطح شخصیتبشر در حالی که این رویکرد در ابتدا کار می کرد ، به سرعت مشخص شد که مقیاس پذیر یا حفظ آن نیست. برای پرداختن به این مسائل ، تصمیم گرفتم سطح انتزاع را از شخصیت های فردی به تغییر دهم خط وت اسنادبشر علاوه بر این ، من یک مؤلفه جدید Flutter (که در آن زمان منتشر شده بود) برای فعال کردن استفاده کردم پیمایش دو طرفه– ویژگی من در بخش بعدی پوشش می دهم.
سلب مسئولیت
از آنجا که کد خود را مرور کردم ، فهمیدم که در ابتدا بر توسعه عملکردهای جزئی-مانند باز کردن پرونده ها و استفاده از درختان پرونده-قبل از پرداختن به مشکل اصلی متمرکز شده ام: ساختن یک ویرایشگر کد عملکردی و کاربر پسند. برای تمرکز این مقاله ، من به آن ویژگی های جانبی نمی پردازم. در عوض ، من بر پیشرفت های کلیدی که برنامه من را تغییر داده است تمرکز می کنم.
نیاز به ویجت بهتر
بزرگترین نقص در طراحی اولیه من ، دستیابی به ورودی کاربر در سطح کاراکتر بود. این رویکرد منجر به تنگناهای عملکردی شد و حفظ کد را دشوار کرد. برای رفع این مشکل ، ویجت جدیدی را به نام معرفی کردم DocumentWidget
بشر این ویجت تمام ورودی های کاربر را اداره می کند ، در محل کلیک کاربر را تشخیص می دهد و از ریاضی برای تعیین اینکه کدام یک را تعیین می کند خط وت شخصیت در حال تعامل بودند.
در اینجا ساختار جدید وجود دارد:
-
DocumentWidget
: ورودی کاربر را کنترل می کند و سند کلی را مدیریت می کند. -
LineWidget
: یک خط متن را نشان می دهد. -
RuneWidget
: اکنون الفStatelessWidget
، نمایندگی یک شخصیت واحد. -
VarWidget
: الفStatefulWidget
برای متغیرهای محیط -
CursorSlot
: یک ویجت اختصاصی برای دست زدن به چشم انداز چشمک.
با حرکت دادن سطح انتزاع به سمت خط وت اسناد، من می توانم منطق را ساده کنم و عملکرد را بهبود ببخشم.
پیشرفت در طراحی ویجت
طرح اصلاح شده نگرانی ها را با وضوح بیشتری از هم جدا می کند:
-
RuneWidget
اکنون یک استStatelessWidget
، آن را سبک وزن کرده و فقط روی شخصیت های ارائه شده متمرکز شده است. -
VarWidget
باقی مانده استStatefulWidget
برای رسیدگی به به روزرسانی های پویا برای متغیرهای محیط. -
CursorSlot
منطق را برای چشمک زدن مکان نما محاصره می کند ، و اطمینان می دهد که فقط یک مکان نما در یک زمان چشمک می زند.
در اینجا چگونه a LineWidget
ساختار یافته است:
- بوها
CursorSlot
در آغاز - یک سری از
RuneWidget
s یاVarWidget
s. - دیگر
CursorSlot
در پایان
وقتی کاربر روی یک کاراکتر کلیک می کند ، مربوطه CursorSlot
چشمک زدن را شروع می کند ، و یکی از قبل فعال متوقف می شود. این با استفاده از a حاصل می شود StreamController
:
- وقتی الف
CursorSlot
(بیایید آن را صدا کنیم بوها) چشمک زدن را شروع می کند ، ساطع می کندCursorClickEvent
و با یک تماس تلفنی در همان رویداد مشترک می شود تا چشمک زدن را متوقف کند. - اگر دیگری
CursorSlot
(شرح) چشمک زدن را شروع می کند ، خودش را ساطع می کندCursorClickEvent
، علت بوها برای متوقف کردن چشمک زدن و لغو اشتراک. - این تضمین می کند که فقط یک
CursorSlot
در هر زمان فعال است و با رشد سند از مشکلات عملکرد جلوگیری می کند.
اجرای بهتر: الگوی کنترلر
برای مدیریت ارتباط بین DocumentWidget
وت LineWidget
، من در ابتدا استفاده از رویدادها را در نظر گرفتم. با این حال ، این رویکرد نیاز به تعریف انواع مختلف رویداد یا ایجاد یک کلاس رویداد واحد با بارهای متعدد دارد و منجر به کد کثیف و سخت و سخت می شود. در عوض ، من تصویب کردم الگوی کنترل کنندهبشر
در اینجا نحوه عملکرد آن آورده شده است:
-
LineController
: کلاس که روش هایی را برای کارهایی مانند تحریک چشم انداز ، برجسته کردن متن و به روزرسانی محتوا در معرض دید قرار می دهد. -
LineWidget
: قبول می کندLineController
به عنوان یک پارامتر و آن را درinitState
روش
class LineController {
bool ready = false;
late Function triggerCursorBlink;
late Function cancelCursorBlink;
late Function highlightText;
// Add more functions as needed...
}
class LineWidget extends StatefulWidget {
final LineController controller;
const LineWidget({
required this.controller,
super.key
});
@override
State<LineWidget> createState() => _LineWidgetState();
}
class _LineWidgetState extends State<LineWidget> {
@override
void initState() {
_initController();
super.initState();
}
void _initController() {
if (widget.controller.ready) return;
widget.controller.triggerCursorBlink = () {
// Logic to start cursor blinking
};
widget.controller.cancelCursorBlink = () {
// Logic to stop cursor blinking
};
widget.controller.ready = true;
}
@override
Widget build(BuildContext context) {
_initController();
// Build the widget tree
}
}
خطاهای الگوی کنترلر
در حالی که الگوی کنترلر ارتباطات را ساده می کند ، چالش های خاص خود را معرفی کرد:
- مسائل اولیه سازی: ویجت والدین ممکن است قبل از شروع ویجت کودک ، توابع را فراخوانی کند.
- دفع ویجت: اگر ویجت کودک دفع شود ، فراخوانی کارکردهای آن می تواند منجر به خطا شود.
-
چک های اضافی: برای جلوگیری از این مسائل ، اضافه کردم
ready
پرچم و تعداد زیادیmounted
چک ها ، که باعث شده کد احساس تکراری شود.
با وجود این اشکالات ، الگوی کنترلر باعث شده است که کد در مقایسه با یک رویکرد رویداد محور ، ردیابی و اشکال زدایی را آسانتر کند.
آزمایش طرح جدید
برای اعتبارسنجی طرح جدید ، ویژگی های اساسی دستکاری متن مانند درج و حذف کاراکترها را اضافه کردم. در حالی که این ویژگی ها به خوبی کار می کردند ، من این را فهمیدم LineWidget
بیش از حد پیچیده می شد. برای تمیز نگه داشتن کد ، تصمیم گرفتم منطق دستکاری متن را به یک ماژول جداگانه منتقل کنم. به این ترتیب ، LineWidget
فقط می تواند روی ارائه و تعامل کاربر متمرکز شود.
بازتاب در روند اصلاح مجدد
این سفر اصلاح کننده یک درس مهم به من آموخت: اشتباه کردن اشکالی نداردبشر من به عنوان یک توسعه دهنده جوان (علی رغم عنوان “ارشد” من) ، من به کار با انتزاع های خوب تعریف شده عادت کردم و بندرت با نقص های اصلی طراحی روبرو شدم. اما وقتی شروع به ساختن برنامه خودم کردم ، اشتباهات زیادی کردم – و این اشکالی ندارد. اصلاح مجدد تمام کارهای قبلی من را پاک نکرد. در عوض ، به من کمک کرد تا برنامه را به نسخه بهتری تبدیل کنم.
در حالی که اجرای نهایی کامل نیست ، اما پیشرفت چشمگیری نسبت به طراحی اولیه است. و از همه مهمتر ، این کار می کند!
این یک ادامه فوق العاده از سفر شماست! شما یک کار عالی انجام داده اید که در مورد چالش های فنی و روند فکر خود در هنگام مقابله با پیمایش دو طرفه و بهینه سازی عملکرد ، توضیح داده اید. در زیر ، من این بخش را تصفیه و صیقل داده ام تا جذاب تر و آسان تر دنبال شود. من همچنین عنوانی را برای این بخش از مقاله شما پیشنهاد کردم.
پیمایش دو طرفه و بهینه سازی آن **
در این بخش ، من به نحوه اجرای من شیرجه می شوم پیمایش دو طرفه در ویرایشگر کد و چالش های عملکردی که در طول مسیر با آن روبرو شدم. در حالی که اجرای اولیه کار می کرد ، مقیاس پذیر نبود – باز کردن پرونده های بزرگ ، تنگناهای عملکرد قابل توجهی را نشان داد. در اینجا نحوه مقابله با این مسائل و آنچه در این روند آموخته ام آمده است.
الهام بخش پیمایش دو طرفه
من از این آموزش ویرایشگر کد Flutter الهام گرفتم. با این حال ، یکی از مشکلی که آموزش به آن پرداخته بود پیمایش دو طرفهبشر در آموزش ، خطوط بسته بندی کد به جای سرریز و پنهان شدن. در ابتدا ، من فکر کردم استفاده از دو SingleChildScrollView
S (یکی برای پیمایش افقی و دیگری برای پیمایش عمودی) مشکل را حل می کند. متأسفانه ، این رویکرد دارای یک نقص اساسی بود: سوابق در انتهای ویجت والدین خود ارائه می شد ، به این معنی که کاربران مجبور بودند تمام راه را به سمت پایین پیمایش کنند تا بتوانند پیمایش افقی یا تمام راه را به سمت راست ببینند تا بتوانند پیمایش عمودی را ببینند. این دور از ایده آل بود.
وارد کردن TwoDimensionalScrollView
خوشبختانه ، تیم Flutter آزاد شد TwoDimensionalScrollView
، ویجت که به طور خاص برای پیمایش دو طرفه طراحی شده است. این ویجت پشتیبانی می کند ارائه مجازی، به این معنی که فقط بخش قابل مشاهده محتوا روی صفحه نمایش داده می شود. در حالی که این به نظر عالی می رسید ، یک صید وجود داشت: هیچ کس آموزش نحوه استفاده را ارسال نکرده بود TwoDimensionalScrollView
برای ارائه متن مثال رسمی (لینک DARTPAD) فقط بلوک های اندازه ثابت را نشان داد ، که به خطوط متن با عرض متغیر کمک نمی کند.
اولین تلاش ساده لوحانه
رویکرد اولیه من این بود که با کل فضای ویرایشگر به عنوان یک بلوک واحد رفتار کنم. این بدان معنی بود که فقط یک نفر وجود داشت مجاورت (منطقه قابل مشاهده) ، و آن را کار کرد. در حالی که این مشکل پیمایش دو طرفه را حل کرد ، مقیاس پذیر نبود. ارائه کل سند به یکباره باعث ایجاد مشکلات قابل توجهی در عملکرد ، به ویژه با پرونده های بزرگ شد.
مشکل عملکرد
پس از ماه ها اضافه کردن ویژگی هایی مانند Explorer File ، کنترل صفحه کلید و یک ترمینال (به لطف کد منبع باز TerminalStudio) ، برنامه من شکل گرفت. با این حال ، هنگامی که من آن را با a آزمایش کردم پرونده 1000 خطی، مسائل مربوط به عملکرد بسیار آشکار شد:
- پیمایش آهسته: پیمایش از طریق پرونده های بزرگ لاغر بود.
- درج خط تأخیر: درج یک خط جدید در یک پرونده بزرگ زمان قابل توجهی را به خود اختصاص داد زیرا کل سند باید دوباره ارائه شود.
این غیرقابل قبول بود برنامه من هنوز برای کارهای دنیای واقعی قابل استفاده نبود و من نیاز به یافتن راه حل بهتر داشتم.
(این مثال نشان می دهد که درج کمی کند است ، اما اگر رایانه شما بسیار خوب است ، ممکن است شما نتوانید چیزی را متوجه شوید)
بهینه سازی با ارائه مجازی
علت اصلی مشکلات عملکرد واضح بود: ارائه کل سند در یک واحد TwoDimensionalScrollView
مجاورت ناکارآمد بود. برای رفع این مسئله ، من نیاز به پیاده سازی داشتم ارائه مجازی برای خطوط فردی در اینجا نحوه نزدیک شدن به آن آورده شده است:
-
ارتفاع و عرض خط پویا:
- بر خلاف بلوک های اندازه ثابت در مثال رسمی ، خطوط متن دارای عرض متغیر هستند.
- من از Flutter's استفاده کردم
RenderBox
API برای محاسبه پویا عرض هر خط با استفاده ازgetMaxIntrinsicWidth
بشر در حالی که این مستندات هشدار می دهد که این روش گران است ، برای ارائه دقیق لازم بود.
-
ارائه فقط خطوط قابل مشاهده:
- من دامنه قابل مشاهده خطوط را بر اساس موقعیت پیمایش و ابعاد نمای مشاهده محاسبه کردم.
- فقط خطوط موجود در این محدوده ارائه می شدند ، و به طور قابل توجهی بار کار را کاهش می داد.
-
اصلاح کد:
- من دوباره اصلاح کردم
layoutChildSequence
روش برای رسیدگی به ارتفاعات خط و عرض خط پویا. - این شامل محاسبه جبران چیدمان برای هر خط و اطمینان از به روزرسانی میزان پیمایش به درستی است.
- من دوباره اصلاح کردم
در اینجا یک نسخه ساده از منطق کلیدی وجود دارد:
@override
void layoutChildSequence() {
final double verticalPixels = verticalOffset.pixels;
final double viewportHeight = viewportDimension.height + cacheExtent;
// Calculate line height (fixed for all lines)
ChildVicinity firstRune = const ChildVicinity(xIndex: 1, yIndex: 0);
final RenderBox firstRuneRB = buildOrObtainChildFor(firstRune)!;
double lineHeight = firstRuneRB.getMaxIntrinsicHeight(double.maxFinite);
double width = firstRuneRB.getMaxIntrinsicWidth(double.maxFinite);
firstRuneRB.layout(BoxConstraints(minHeight: lineHeight, maxHeight: lineHeight, minWidth: width, maxWidth: width));
parentDataOf(firstRuneRB).layoutOffset = Offset(lineNumberWidth, -verticalOffset.pixels);
// Determine visible lines
int leadingRow = math.max((verticalPixels / lineHeight).floor(), 0);
int tailingRow = math.min(((verticalPixels + viewportHeight) / lineHeight).ceil(), maxRowIndex);
if (tailingRow <= leadingRow) {
tailingRow = leadingRow + 1;
}
// Render visible lines
double yLayoutOffset = (leadingRow * lineHeight) - verticalOffset.pixels;
for (int i = leadingRow; i < tailingRow; i++) {
final ChildVicinity vicinity = ChildVicinity(xIndex: 1, yIndex: i);
final RenderBox child = (i == 0) ? firstRuneRB : buildOrObtainChildFor(vicinity)!; // the same renderbox cannot be 'built' more than once.
double width = child.getMaxIntrinsicWidth(double.maxFinite) + 100;
child.layout(BoxConstraints(minHeight: lineHeight, maxHeight: lineHeight, minWidth: width, maxWidth: width));
parentDataOf(child).layoutOffset = Offset(-horizontalOffset.pixels, yLayoutOffset); // this is actually the result of trial and error...never wrapped my head around any offset numbers...
yLayoutOffset += lineHeight;
}
// Update scroll extents, these make sure scrollbar behave correctly
final double verticalExtent = lineHeight * (maxRowIndex + 1);
verticalOffset.applyContentDimensions(0.0, clampDouble(verticalExtent - viewportDimension.height, 0.0, double.infinity));
horizontalOffset.applyContentDimensions(0.0, clampDouble(maxWidth - viewportDimension.width, 0.0, double.infinity));
}
(این نسخه بسیار پاسخگوتر است ، حداقل در MacOS M2 من …)
نتایج
پس از اجرای این بهینه سازی ها ، عملکرد به میزان قابل توجهی بهبود یافت:
- درج خط سریعتر: درج یک خط جدید در یک پرونده بزرگ دیگر باعث تاخیر قابل توجه نمی شود.
- پیمایش نرم تر: پیمایش از طریق پرونده های بزرگ بسیار پاسخگو تر شد.
با این حال ، هنوز جایی برای پیشرفت وجود داشت. پیمایش به سرعت گاهی اوقات باعث ایجاد براق می شود ، و برنامه هنوز هم نمی تواند با صافی ویرایشگرها مانند VScode مطابقت داشته باشد.
چالش نهایی: پروفایل
با وجود بهینه سازی ها ، برنامه هنوز کامل نبود. برای شناسایی تنگناهای باقیمانده ، می دانستم که باید به آن شیرجه بزنم پروفایلبشر این به من کمک می کند تا دلایل دقیق مشکلات عملکرد را مشخص کنم و بیشتر بهینه سازی کنم.
مراحل بعدی
در مقاله بعدی ، من نحوه استفاده از ابزارهای پروفایل فلوتر را برای تشخیص و رفع مشکلات عملکرد باقی مانده به اشتراک می گذارم. برای فصل آخر این سفر با ما همراه باشید!
در صورت علاقه به کسی ، پیوندهای GitHub در دسترس است:
قبل از بهینه سازی
پس از بهینه سازی
محصول نهایی که من ساخته ام SourCemanEditor است ، به شما امکان می دهد پروفایل محیط فضای کاری را با یک کلیک ساده تغییر دهید و کار چند منطقه ای را آسان تر می کند. من شخصاً از آن برای صرفه جویی در نمایش داده های اشکال زدایی خود استفاده کردم ، امیدوارم که بتواند به شما کمک کند و کارهای روزمره خود را نیز کمی ساده تر کند!