- เนื่องจาก ข้อจำกัดของ Howl editor เดิม (หยุดพัฒนา, ค้นหาช้า, ไม่รองรับ SSH, ไม่รองรับเทอร์มินัล) จึงพัฒนา TUI text editor ตัวใหม่ขึ้นมาเอง
- ลองใช้ editor มาแล้ว 13 ตัว เช่น Helix, VS Code, Vim, Neovim, Emacs แต่ก็ยังไม่มีตัวไหนตอบโจทย์ สัมผัสในการควบคุม (Fingerspitzengefühl) ที่ต้องการ
- ในช่วงแรก เลือกทำเฉพาะ ฟีเจอร์ขั้นต่ำที่ปรับให้เข้ากับตัวเอง ก่อน แล้วค่อยขยายทีละน้อย โดยเลื่อนเรื่องประสิทธิภาพ, Unicode และการรองรับหลายภาษาออกไปก่อน
- ระหว่างพัฒนา ได้ลงมือทำ regex engine, file browser, การเรนเดอร์แบบ TUI, การรวม terminal buffer ขึ้นมาเอง
- มีการใช้ เทคนิคปรับแต่งประสิทธิภาพ จำนวนมากในส่วนค้นหาทั้งโปรเจกต์, syntax highlighting, การจัดการแคช และการกระจายงานแบบมัลติเธรด
- สุดท้ายได้เครื่องมือที่เข้ากับ workflow ของตัวเองอย่างสมบูรณ์ พร้อมทั้ง ได้ทั้งประสิทธิภาพการทำงานและความสนุกในการเขียนโปรแกรมกลับคืนมา
ข้อจำกัดของ editor เดิมและการมองหาทางเลือก
- ปัญหาของ Howl editor ที่ใช้งานมาราว 10 ปี เป็นแรงผลักดันให้เริ่มพัฒนาขึ้นเอง
- ตัวโปรแกรม หยุดพัฒนา มาหลายปี จึงต้องดูแล fork เอง แต่เพราะเขียนด้วย MoonScript ทำให้แก้ไขเชิงลึกได้ยาก
- ประสิทธิภาพการ ค้นหาไฟล์ทั้งโปรเจกต์ ไม่ดีพอ จนรบกวน workflow
- เพราะเป็น GUI editor จึงใช้งานระยะไกลผ่าน SSH ไม่ได้
- ไม่มี integrated terminal ทำให้สั่งคำสั่งภายนอกแล้วโต้ตอบแบบสดไม่ได้ และยังไม่รองรับ ANSI escape code ส่วนใหญ่
- ลองใช้ editor มาแล้ว 13 ตัว ได้แก่ Helix, VS Code, Sublime Text, Vim, Zed, Neovim, Emacs, Geany, Micro, Lite XL, Lapce, GNOME Builder, Kakoune
- แต่ละตัวมีข้อดีของตัวเอง แต่ยังไม่ตอบโจทย์ สัมผัสในการควบคุม (Fingerspitzengefühl) ที่ต้องการ
- ใช้ Helix นานที่สุด แต่ผ่านไปหนึ่งเดือนก็หมดความสนใจ
กลยุทธ์การพัฒนาในช่วงแรก
- ในช่วงเริ่มต้น จำกัดขอบเขตให้เล็กที่สุด
- ตัดฟีเจอร์สำหรับผู้ใช้อื่นออกทั้งหมด และ hardcode การตั้งค่าทุกอย่าง
- เลื่อนการปรับแต่งประสิทธิภาพออกไปก่อน และเริ่มจากบัฟเฟอร์แบบ
String
- ไม่รองรับ Unicode grapheme แบบสมบูรณ์ โดยมองว่าแค่ให้สัญลักษณ์
£ กินพื้นที่หนึ่งคอลัมน์ได้ก็พอ
- syntax highlighting รองรับเฉพาะไม่กี่ภาษาที่ใช้บ่อย ส่วนที่เหลือใช้การไฮไลต์แบบทั่วไปตามตัวคั่นแทน
- ในความพยายามครั้งที่สอง เคยสร้าง TUI framework แบบง่ายขึ้นมาก่อน แต่เมื่อเวลาผ่านไปก็รื้อออกไปเกือบหมด และเปลี่ยนไปใช้แนวทางที่ตรงไปตรงมาและละเอียดกว่ามาก
การทำ Dogfooding
- เมื่อ editor ไปถึง จุดขั้นต่ำที่ใช้งานได้ คือเปิด แก้ไข และบันทึกไฟล์เดี่ยวได้ ก็เริ่มทำ 3 อย่าง
- ใช้ editor ของตัวเองแทน
nano แบบ บังคับใช้จริง เวลาจะแก้ไฟล์ระบบหรือจดบันทึก
- ทุกครั้งที่เจอฟีเจอร์ที่ขาด บั๊ก พฤติกรรมแปลก หรือข้อจำกัด ก็จดลงใน
README.md ของโปรเจกต์
- ถ้าเป็นปัญหาที่น่าหงุดหงิดมาก ก็ แก้ทันที
- การทำทั้งสามข้อนี้ทำให้ปริมาณงานเพิ่มจากเดือนละ 1 ชั่วโมงเป็น หลายชั่วโมงต่อสัปดาห์
- จากโค้ดทั้งหมดราว 10,000 บรรทัด เกือบทั้งหมดถูกเขียนขึ้นในช่วง 6 เดือนหลังสุด
การควบคุมเคอร์เซอร์
- การควบคุมเคอร์เซอร์เป็น ส่วนที่ทำยากมาก
- คีย์ลัดอย่าง
ctrl + shift + left ดูเป็นเรื่องธรรมดาสำหรับผู้ใช้ แต่ตรรกะภายในซับซ้อนมาก
- คำแนะนำสำคัญคือการสร้างอินพุตระดับสูงจาก การประกอบ primitive operation
- ตัวอย่าง: backspace แบบลบทีละคำ → แยกเป็นเลื่อนเคอร์เซอร์ทีละคำ + เลือกช่วง + ลบ
- เวลาทำ undo/redo ต้อง จัด 3 การกระทำนี้เป็นกลุ่มเดียวกัน จึงจะได้ผลลัพธ์ที่เป็นธรรมชาติ
- ทำให้เข้าใจว่าเหตุใด modal editor จึง เปิดเผย primitive operation เหล่านี้ให้ผู้ใช้โดยตรง
File browser
- file browser ของ Howl คือเหตุผลชี้ขาดที่ทำให้ย้ายไป editor อื่นไม่ได้
- fuzzy filter ที่อัปเดตทันที ทำได้ดีมาก จนส่วนใหญ่พิมพ์แค่ 1–2 คีย์ก็หาไฟล์ที่ต้องการเจอ
- ถ้าไฟล์ยังไม่มีอยู่ ก็สามารถ สร้าง inline ได้ทันที
- เมื่อพิมพ์
~/ จะ สลับไปยัง home directory อัตโนมัติ
- แสดง ตัวอย่างไฟล์ล่วงหน้า ที่กำลังจะเปิดจากหน้าต่างแก้ไขหลัก
- ไม่พอใจกับวิธีที่ editor อื่นแก้ปัญหาการเปิดไฟล์ด้วยการ พึ่งเมาส์, ใช้กล่องโต้ตอบ GTK มาตรฐาน, หรือเดาชื่อไฟล์
- เมื่อลงมือทำเอง พบว่าไม่จำเป็นต้องใช้วิธีซับซ้อนอย่าง Levenshtein distance แค่เกณฑ์ง่าย ๆ 3 ข้อก็พอ
- ชื่อไฟล์ ขึ้นต้นด้วย สตริงกรองหรือไม่
- ชื่อไฟล์ มี สตริงกรองอยู่หรือไม่
- เวลา แก้ไข/เข้าถึงล่าสุด
- อนุญาตการจับคู่แบบไม่สนตัวพิมพ์ใหญ่เล็ก แต่ถ้าตรงตัวพิมพ์ด้วยก็ เพิ่มอันดับเล็กน้อย
- แม้ในโปรเจกต์ที่มีไฟล์หลายหมื่นไฟล์ หลังพิมพ์ 2 คีย์ก็มีโอกาสราว 95% ที่ไฟล์เป้าหมายจะติดอยู่ใน 2 อันดับแรก
Regex engine
- regex ถูกใช้ใน 3 ส่วนคือ ค้นหาทั้งโปรเจกต์, syntax highlighting และค้นหาในบัฟเฟอร์
- เหตุผลที่ไม่ใช้ crate
regex-automata เดิม แต่เลือกทำเอง
- ต้องรองรับ edge case แบบ ไวต่อบริบท เช่น raw string syntax ของ Rust
- โปรเจกต์นี้เองก็เป็นการฝึก สร้างและทำความเข้าใจ stack ของตัวเอง
- เวอร์ชันแรกใช้ parsing crate
chumsky แยกไวยากรณ์ regex แล้ว เดิน AST ทุกครั้งต่อหนึ่งตัวอักษร ซึ่งช้ามาก
- หลังจากนั้นจึงค่อย ๆ ปรับแต่งทีละขั้น
- single-pass optimizer: แปลงกลุ่มจับคู่ตัวอักษรที่ปรากฏซ้ำให้เป็นโหนด
String เดียว เพื่อค้นหาสตริงตรง ๆ ได้
- ดึง prefix ร่วม: เช่นใน
hel[(lo)p] ตรวจพบ prefix ร่วมคือ hel แล้วค่อยจับคู่เฉพาะตำแหน่งนั้น → ช่วยเพิ่มประสิทธิภาพการค้นหาทั้งโปรเจกต์อย่างมาก
- เขียน AST walker ใหม่เป็น threaded code VM ที่อิงการเรียกแบบไดนามิกของ Rust
- แปลง threaded code VM ให้อยู่ในรูป CPS (Continuation-Passing Style) โดยให้แต่ละคำสั่ง VM tail-call ไปยังคำสั่งถัดไป เพื่อใช้ประโยชน์จากการ optimize ของคอมไพเลอร์
- ห่อการเรียกฟังก์ชันไดนามิกที่ช้าของ Rust โดยไม่ต้อง lookup vtable ทำให้ codegen ของคำสั่ง regex หลายแบบเหลือเพียงไม่กี่ machine instruction
- ทำให้คำสั่ง regex ให้ได้มากที่สุดทำงานในระดับ ไบต์ แทน Unicode codepoint และด้วยการออกแบบของ UTF-8 เทคนิค optimize สำหรับ ASCII ก็ยังใช้ได้กับ codepoint หลายไบต์
- เคยลองคอมไพล์ไปเป็น jump LUT chain ด้วย แต่ผล benchmark เร็วกว่า threaded code แค่ 20–30% และยืดหยุ่นน้อยลงมาก จึงไม่เลือกใช้
- ผลลัพธ์สุดท้าย: ใน syntax highlighting ที่ซับซ้อนที่สุดสำหรับ Rust สามารถไฮไลต์ไฟล์ binding ที่สร้างอัตโนมัติขนาด 50,000 บรรทัด แบบ clean state ได้เสร็จทั้งไฟล์ในเวลา ไม่ถึง 10 มิลลิวินาที
แคชสำหรับ syntax highlighting
- ตอนแรกใช้วิธี ไฮไลต์ทั้งไฟล์ใหม่ทุกครั้ง เมื่อมีการเปลี่ยนแปลง แต่ทำให้ไฟล์ใหญ่ช้าลง
- จึงสร้าง on-demand token highlighting cache
- ไฮไลต์ token เป็น chunk ขนาดใกล้เคียงกัน
- เมื่อบัฟเฟอร์เกิดการเปลี่ยนแปลง (damage) จะ invalidate เฉพาะ chunk ที่ทับตำแหน่งนั้นหรืออยู่ถัดจากจุดนั้น
- แม้ในกรณีเลวร้ายที่สุด (แก้ตรงกลางไฟล์ใหญ่) สถานะการไฮไลต์ก่อนจุด damage ก็ยังคงอยู่ และข้อมูลด้านล่างหน้าจอที่ ยังไม่มีการร้องขอ ก็ไม่ต้องประมวลผล
- เพราะเป็นแนวทาง demand-driven จึงทำงานได้ดีแม้มี หลายพาเนล ที่เปิดดูคนละส่วนของบัฟเฟอร์เดียวกัน
การค้นหาทั้งโปรเจกต์
- กระบวนการค้นหามี 4 ขั้นตอน
- ค้นหาไดเรกทอรี
.git/ ย้อนขึ้นไปจากไดเรกทอรีปัจจุบันเพื่อกำหนด project root
- เดินไดเรกทอรีทั้งหมดใต้ project root แบบ recursive แล้ว จับคู่แพตเทิร์นค้นหากับเนื้อหาไฟล์
- สำหรับแต่ละผลที่ตรงกัน จะดึง snippet ของไฟล์ออกมาและใช้ syntax highlighting เพื่อแสดงตัวอย่างผลลัพธ์
- จัดอันดับผลลัพธ์ตาม ระยะการเดินทาง จาก path ปัจจุบัน (ไฟล์ที่ใกล้กว่าจะอยู่สูงกว่า)
- ใช้กฎ filtering เริ่มต้นเพื่อหลบ build directory และส่วนอื่นที่ไม่เกี่ยวข้อง
- ประมวลผลแบบ multithreaded โดยใช้การแจกงานระหว่างเธรดแนว work-stealing
- มีการแก้ปัญหา การตรวจจับการสิ้นสุด ในโครงสร้างพิเศษที่ทุกเธรดเป็นทั้งผู้ผลิตและผู้บริโภค
- เธรดที่รออยู่จะเพิ่ม atomic counter และหากตัวนับถึงจำนวน worker พร้อมกับคิวงานว่าง ก็ ยุติทั้งหมด
- ด้วยการ optimize regex และความเร็วของ SSD สมัยใหม่ การค้นหาแพตเทิร์นง่าย ๆ ใน codebase ใหญ่อย่าง Veloren แทบจะเสร็จทันที
- จาก flamegraph พบว่าเวลาส่วนใหญ่เป็น IO-bound
- การที่ค้นหา codebase ขนาดใหญ่ ได้เร็วพอ ๆ กับความคิด จากใน editor โดยตรง ช่วยเพิ่ม productivity อย่างมาก
Terminal emulator buffer
- ใน editor แบบใช้พาเนล ความสามารถในการใช้พาเนลหนึ่งเป็น หน้าต่างเทอร์มินัล สะดวกมาก
- เดิมตั้งใจจะทำ ANSI parser เอง แต่การรองรับฟีเจอร์ terminal rendering สมัยใหม่อย่าง OSC52 และ Kitty keyboard protocol มีขอบเขตกว้างมาก
- จึงใช้ crate
alacritty_terminal เพื่อนำ parser ของ escape sequence และตรรกะจัดการสถานะเทอร์มินัลของ Alacritty terminal emulator มาใช้ซ้ำ
- ผลลัพธ์คือสามารถแทนฟีเจอร์หลักของ
screen/tmux ได้ พร้อมทั้งรองรับ escape sequence ได้หลากหลายกว่า
การปรับแต่งการเรนเดอร์
- แม้จะเป็น TUI แต่เมื่อเชื่อมต่อจากมือถือระยะไกล แบนด์วิดท์ ก็ยังสำคัญ
- ใช้ double buffering: เก็บสำเนาภายในของหน้าจอเทอร์มินัลไว้สองชุด
- ตอนวาดใหม่จะเทียบกับเฟรมก่อนหน้า แล้วส่งออก ANSI escape sequence เฉพาะเซลล์ที่เปลี่ยน
- sequence อย่างการย้ายเคอร์เซอร์หรือเปลี่ยนโหมดสไตล์ ก็จะส่งเฉพาะเมื่อ จำเป็นจริงเท่านั้น
- ใน terminal emulator ส่วนใหญ่ (ยกเว้น Ghostty) การ
cat ไฟล์ใหญ่จากพาเนลเทอร์มินัลของ editor แล้วปิด editor กลับ เร็วกว่า การ cat ตรงจาก host terminal เอง
- เพราะ
alacritty_terminal ช่วย กันต้นทุนการประมวลผล stdout bytes ต่อ terminal ฝั่งโฮสต์ไว้ได้
บทสรุป: จงสร้างเครื่องมือของตัวเอง
- editor ที่ทำขึ้นเองได้กลายเป็น เครื่องมือที่เข้ากับ workflow ของตัวเองอย่างสมบูรณ์
- ผู้เขียน ไม่เห็นด้วย กับความเชื่อที่ว่าการสร้าง editor/เครื่องมือเองเป็นความลำบากที่ไร้ความหมาย
- มีข้อดี 4 ประการ
- ปรับได้พอดีอย่างสมบูรณ์: ทำเฉพาะสิ่งที่ต้องการเท่านั้น ไม่มากไปไม่น้อยไป
- เรียนรู้เทคโนโลยีหลากหลาย: ได้ความเข้าใจเชิงลึกในเรื่อง regex, ANSI, pseudoterminal (pty), การออกแบบ TUI, รายละเอียดของ UTF-8 และ เทคนิคที่นำไปใช้ได้กว้าง
- เพิ่ม productivity ระยะยาว: เข้าใจเครื่องมือของตัวเองอย่างสมบูรณ์ และฝังฟีเจอร์ที่เข้ากับ workflow ส่วนตัวไว้ได้ จึงลดแรงเสียดทานระหว่างคนกับเครื่องมือ
- ความสนุกอย่างแท้จริง: การแก้ปัญหาที่จบในตัวเองและสัมผัสผลลัพธ์ได้ด้วยปลายนิ้ว ปลุกความรักในการเขียนโปรแกรมกลับมาอีกครั้ง ถึงขั้นยิ้มกว้างและหัวเราะคนเดียวขณะเขียนโค้ดเป็นครั้งแรกในรอบหลายปี
- ต่อให้ไม่ใช่ text editor ก็ได้ ผู้เขียนแนะนำให้ ลองสร้างเครื่องมือของตัวเอง และเน้นว่าอย่าโยนส่วนที่ยากให้กล่องสถิติ (เช่น AI) แต่ให้ สนุกกับความท้าทายนั้นเอง
5 ความคิดเห็น
จริงๆ แล้วสิ่งที่น่าทึ่งที่สุดในบทความนี้คือคำคำหนึ่งนี่แหละ
Fingerspitzengefühl
Finger(นิ้วมือ) + Spitzen(ปลาย) + Gefühl(ความรู้สึก/สัมผัส)
ภาษาเยอรมันนี่สุดยอดจริงๆ ที่มีคำไว้ใช้บอกความรู้สึกในการควบคุมด้วยปลายนิ้ว.. เฮ้อ..
fingerก็เป็นภาษาเยอรมันเหมือนกันสินะ ผมนึกว่าเป็นภาษาอังกฤษ...อยู่ในตระกูลภาษาเดียวกัน จึงมีคำศัพท์พื้นฐานที่ใช้ร่วมกันอยู่มาก
เป็นภาษาเยอรมันที่สามารถผสมคำได้อย่างไร้ขีดจำกัดเลยครับ 555
ความคิดเห็นจาก Hacker News
อ่านแล้วเพลินมากตลอดทั้งบทความ ผมถึงกับแนะนำเพื่อน ๆ ให้ลองสร้าง text editor ของตัวเองดู
ผมใช้ editor ของตัวเองชื่อ ‘Left’ มาเกือบ 10 ปีแล้ว ตอนแรกมันยังไม่สมบูรณ์แบบ แต่ผมก็ค่อย ๆ พัฒนา Left ด้วยการใช้ Left แก้ไข Left เอง ความสุขที่ได้เปิดเครื่องมือที่ตัวเองสร้างขึ้นมาทุกเช้านั้นคุ้มค่ากับเวลาที่ลงไปเป็น 20 เท่า
มีคำกล่าวว่า “ในชีวิตควรสร้างบ้าน ปลูกต้นไม้ และสร้าง editor สักครั้ง” ผมเริ่มจากอย่างสุดท้ายก่อน
เป็นประโยคจาก Vip ซึ่งเป็น editor สไตล์ Vi ที่สร้างบน PicoLisp
ผมก็เคยสร้าง text editor เองตั้งแต่ต้นเหมือนกัน เพราะฟีเจอร์มีเยอะเลยใช้เครื่องมือภายนอกอย่าง LSP, tree-sitter, fzf อย่างเต็มที่
ออกแบบให้ custom ได้ง่ายแบบสาย suckless แค่แก้โค้ดก็พอ
ช่วงสองสามสัปดาห์แรกบั๊กเพียบ แต่ยิ่งแก้ก็ยิ่งเสถียรขึ้นเรื่อย ๆ ดูโปรเจกต์ของผม hat ได้
มีใครพอจะแนะนำ library สำหรับแก้ไขข้อความ ได้บ้างไหม?
GUI เป็นสิ่งจำเป็น เลยต้องแตะทั้ง font renderer และ graphics context ด้วยตัวเอง
ถ้าเป็นคอนโซลอย่างเดียวผมใช้ไม่ได้ แต่ถ้ามีแค่ GUI ก็ไม่มีความสามารถในการแก้ไข เลยต้องมีทั้งสองอย่าง
น่าแปลกที่หายากมากที่จะเจอ library แบบ pure API ที่ตอบโจทย์แบบนี้
ส่วนใหญ่ไม่ก็เป็น editor ที่เสร็จสมบูรณ์ไปเลย ไม่ก็ใหญ่ระดับ framework
ผมแค่อยากได้ editing engine พื้นฐานที่จัดการ ไฟล์ข้อความขนาดใหญ่ ได้เร็ว ๆ เท่านั้น
ยังห่อให้ดูเหมือน native UI ที่ใช้ ghostty ได้ด้วยเครื่องมืออย่าง trolley
แต่ถ้าเป็น SDL คู่กับ SDL_ttf ถือว่าโอเคมาก SDL3_ttf ก็ปรับปรุงเรื่องการจัดการสตริงดีขึ้นด้วย
ผมลองเขียน editor “kilo” ของ antirez ขึ้นมาใหม่
ทั้งโค้ดต้นฉบับและบทสอนทำไว้ดีมาก เป็นโปรเจกต์ที่ยอดเยี่ยมสำหรับเรียนรู้ โหมดเทอร์มินัล และพื้นฐานของภาษา C
ทำให้นึกถึงยุค 90 ที่ผมเคยสร้าง editor เองไว้ใช้กับไฟล์ COBOL และ ASM
มีทั้งsyntax highlighting, buffering เร็ว ๆ แม้แต่ screensaver ก็ยังมี
มันรันบน Pentium 120 ได้ แล้วในความทรงจำผมมันเร็วกว่า VSCode ตอนนี้เป็นพันเท่า
ตอนนั้นผมยังเขียนแท็ก HTML เป็นตัวพิมพ์ใหญ่ทั้งหมดอยู่เลย
นี่คือ editor zte ที่เจ้าของบทความทำขึ้น
ประโยคที่ว่า “จงต้านทานความยั่วยวนที่จะผลักส่วนที่ยากเข้าไปไว้ในกล่องสถิติ” สะดุดใจผมมาก
ผมก็ใช้ editor ที่ทำเอง เหมือนกัน คนอื่นอาจไม่ได้สนใจมาก แต่คุณค่าที่ได้จากเครื่องมือที่สร้างเองนั้นสูงมาก
ยังมีฟังก์ชัน ‘browser’ ง่าย ๆ ที่กด F5 เพื่อเปิดลิงก์ได้ด้วย
Josh Barretto คืออัจฉริยะที่สร้าง พอร์ต Super Mario 64 GBA ขึ้นมา ถ้าเป็น editor ของเขาผมยินดีลองใช้มาก