دروس بروتوكول خادم اللغة: من VSCode إلى Vim
نشرت: 2022-03-11الأداة الرئيسية في كل أعمالك هي على الأرجح ملفات نصية عادية. فلماذا لا تستخدم برنامج المفكرة لإنشائها؟
يعتبر تمييز بناء الجملة والتنسيق التلقائي مجرد غيض من فيض. ماذا عن الفحص وإكمال الكود وإعادة البناء شبه التلقائي؟ هذه كلها أسباب وجيهة لاستخدام محرر كود "حقيقي". هذه عناصر حيوية في حياتنا اليومية ، لكن هل نفهم كيف تعمل؟
في هذا البرنامج التعليمي لبروتوكول خادم اللغة ، سوف نستكشف هذه الأسئلة قليلاً ونكتشف ما الذي يجعل محرري النصوص لدينا يحددون. في النهاية ، سنقوم معًا بتنفيذ خادم لغة أساسي جنبًا إلى جنب مع أمثلة من عملاء VSCode و Sublime Text 3 و Vim.
المجمعين مقابل خدمات اللغة
سنتخطى إبراز الصيغة والتنسيق في الوقت الحالي ، والذي يتم التعامل معه من خلال التحليل الثابت - وهو موضوع مثير للاهتمام في حد ذاته - والتركيز على الملاحظات الرئيسية التي نحصل عليها من هذه الأدوات. هناك فئتان رئيسيتان: المجمعين والخدمات اللغوية.
المترجمون يأخذون التعليمات البرمجية المصدر الخاصة بك ويبصقون شكل مختلف. إذا كانت الشفرة لا تتبع قواعد اللغة ، فسيرجع المترجم الأخطاء. هذه مألوفة تماما. تكمن المشكلة في هذا في أنه عادةً ما يكون بطيئًا ومحدود النطاق. ماذا عن تقديم المساعدة أثناء إنشاء الشفرة؟
هذا ما تقدمه خدمات اللغة. يمكن أن يقدموا لك نظرة ثاقبة حول قاعدة التعليمات البرمجية الخاصة بك أثناء العمل ، وربما أسرع بكثير من تجميع المشروع بأكمله.
نطاق هذه الخدمات متنوع. يمكن أن يكون شيئًا بسيطًا مثل إعادة قائمة بجميع الرموز في المشروع ، أو شيئًا معقدًا مثل إعادة الخطوات إلى كود إعادة البناء. هذه الخدمات هي السبب الرئيسي لاستخدامنا لمحرري الكود. إذا أردنا فقط تجميع الأخطاء ومشاهدتها ، فيمكننا فعل ذلك ببضع ضغطات على المفاتيح. تعطينا الخدمات اللغوية مزيدًا من الأفكار وبسرعة كبيرة.
المراهنة على محرر نصوص للبرمجة
لاحظ أننا لم نستدعي برامج تحرير نصوص معينة حتى الآن. دعنا نشرح لماذا بمثال.
لنفترض أنك طورت لغة برمجة جديدة تسمى لابين. إنها لغة جميلة والمترجم يعطي رسائل خطأ شبيهة بـ Elm. بالإضافة إلى ذلك ، يمكنك توفير إكمال التعليمات البرمجية والمراجع ومساعدة إعادة البناء والتشخيص.
أي رمز / محرر نصوص تدعمه أولاً؟ ماذا بعد ذلك؟ لديك معركة شاقة تقاتل لجعل الناس يتبنونها ، لذلك تريد أن تجعلها سهلة قدر الإمكان. لا تريد أن تختار المحرر الخاطئ وتفوت المستخدمين. ماذا لو ابتعدت عن محرري الكود وركزت على تخصصك - اللغة وخصائصها؟
خوادم اللغة
أدخل خوادم اللغة . هذه هي الأدوات التي تتحدث إلى عملاء اللغة وتوفر الأفكار التي ذكرناها. إنهم مستقلون عن محرري النصوص للأسباب التي وصفناها للتو مع وضعنا الافتراضي.
كالعادة ، طبقة أخرى من التجريد هي فقط ما نحتاجه. تعد هذه بقطع الاقتران الوثيق بين أدوات اللغة ومحرري الكود. يمكن لمنشئي اللغة التفاف ميزاتهم في خادم مرة واحدة ، ويمكن لمحرري الشفرات / النصوص إضافة ملحقات صغيرة لتحويل أنفسهم إلى عملاء. إنه فوز للجميع. لتسهيل ذلك ، على الرغم من ذلك ، نحتاج إلى الاتفاق على كيفية تواصل هؤلاء العملاء والخوادم.
محظوظ بالنسبة لنا ، هذا ليس افتراضيا. بدأت Microsoft بالفعل بتعريف بروتوكول خادم اللغة.
كما هو الحال مع معظم الأفكار العظيمة ، فقد نشأ عن الضرورة بدلاً من التبصر. بدأ العديد من محرري الأكواد بالفعل في إضافة دعم لميزات اللغة المختلفة ؛ تم الاستعانة بمصادر خارجية لبعض الميزات لأدوات خارجية ، وبعضها يتم تحت غطاء محرك السيارة داخل المحررين. ظهرت مشكلات قابلية التوسع ، وتولت Microsoft زمام المبادرة في تقسيم الأشياء. نعم ، مهدت Microsoft الطريق لنقل هذه الميزات خارج برامج تحرير التعليمات البرمجية بدلاً من تكديسها داخل VSCode. كان بإمكانهم الاستمرار في بناء محررهم ، وحبس المستخدمين - لكنهم أطلقوا سراحهم.
بروتوكول خادم اللغة
تم تعريف بروتوكول خادم اللغة (LSP) في عام 2016 للمساعدة في فصل أدوات ومحرري اللغة. لا يزال هناك العديد من بصمات VSCode عليها ، لكنها خطوة رئيسية في اتجاه اللاأدرية المحرر. دعونا نفحص البروتوكول قليلاً.
العملاء والخوادم — يفكر في محرري التعليمات البرمجية وأدوات اللغة — يتواصلون في رسائل نصية بسيطة. تحتوي هذه الرسائل على رؤوس تشبه HTTP ومحتوى JSON-RPC ، وقد تنشأ من العميل أو الخادم. يحدد بروتوكول JSON-RPC الطلبات والاستجابات والإخطارات وبعض القواعد الأساسية حولها. الميزة الرئيسية هي أنه مصمم للعمل بشكل غير متزامن ، بحيث يمكن للعملاء / الخوادم التعامل مع الرسائل خارج الترتيب وبدرجة من التوازي.
باختصار ، يسمح JSON-RPC للعميل بطلب برنامج آخر لتشغيل طريقة مع معلمات وإرجاع نتيجة أو خطأ. يبني LSP على هذا ويحدد الطرق المتاحة وهياكل البيانات المتوقعة وعدد قليل من القواعد حول المعاملات. على سبيل المثال ، هناك عملية مصافحة عندما يبدأ العميل في تشغيل الخادم.
الخادم ذو الحالة ويقصد به فقط التعامل مع عميل واحد في كل مرة. لا توجد قيود صريحة على الاتصال ، ومع ذلك ، يمكن تشغيل خادم اللغة على جهاز مختلف عن جهاز العميل. من الناحية العملية ، سيكون ذلك بطيئًا جدًا بالنسبة للتعليقات في الوقت الفعلي. تعمل خوادم اللغة والعملاء مع نفس الملفات وهي ثرثرة جدًا.
يحتوي LSP على قدر مناسب من الوثائق بمجرد أن تعرف ما الذي تبحث عنه. كما ذكرنا ، فإن الكثير من هذا مكتوب في سياق VSCode ، على الرغم من أن الأفكار لها تطبيق أوسع بكثير. على سبيل المثال ، تتم كتابة مواصفات البروتوكول كلها في TypeScript. لمساعدة المستكشفين الذين ليسوا على دراية بـ VSCode و TypeScript ، إليك كتاب تمهيدي.
أنواع رسائل LSP
هناك العديد من مجموعات الرسائل المحددة في بروتوكول خادم اللغة. يمكن تقسيمها تقريبًا إلى "ميزات إدارية" و "ميزات اللغة". تحتوي رسائل الإدارة على تلك المستخدمة في مصافحة العميل / الخادم ، وفتح / تغيير الملفات ، وما إلى ذلك. والأهم من ذلك ، أن هذا هو المكان الذي يشارك فيه العملاء والخوادم الميزات التي يتعاملون معها. بالتأكيد ، توفر اللغات والأدوات المختلفة ميزات مختلفة. هذا يسمح أيضا للتبني المتزايد. يقوم موقع Langserver.org بتسمية نصف دزينة من الميزات الأساسية التي يجب أن يدعمها العملاء والخوادم ، واحدة منها على الأقل مطلوبة لعمل القائمة.
ما يهمنا هو ميزات اللغة في الغالب. من بين هذه الميزات ، هناك ميزة يجب ذكرها على وجه التحديد: رسالة التشخيص. التشخيصات هي واحدة من السمات الرئيسية. عندما تفتح ملفًا ، يُفترض غالبًا أن هذا سيتم تشغيله. يجب أن يخبرك المحرر الخاص بك إذا كان هناك خطأ ما في الملف. الطريقة التي يحدث بها هذا مع LSP هي:
- يفتح العميل الملف ويرسل
textDocument/didOpen
إلى الخادم. - يحلل الخادم الملف ويرسل إشعار
textDocument/publishDiagnostics
. - يحلل العميل النتائج ويعرض مؤشرات الخطأ في المحرر.
هذه طريقة سلبية للحصول على رؤى من خدمات لغتك. من الأمثلة الأكثر نشاطًا العثور على جميع المراجع للرمز الموجود أسفل المؤشر. هذا من شأنه أن يحدث مثل:
- يرسل العميل
textDocument/references
إلى الخادم ، مع تحديد موقع في ملف. - يكتشف الخادم الرمز ، ويحدد المراجع في هذا الملف وغيره ، ويستجيب بقائمة.
- يعرض العميل المراجع للمستخدم.
أداة القائمة السوداء
يمكننا بالتأكيد البحث في تفاصيل بروتوكول خادم اللغة ، لكن دعنا نترك ذلك لمنفذي العميل. لتعزيز فكرة فصل أداة المحرر واللغة ، سنلعب دور منشئ الأداة.
سنبقي الأمر بسيطًا ، وبدلاً من إنشاء لغة وميزات جديدة ، سنلتزم بالتشخيصات. تعتبر عمليات التشخيص مناسبة تمامًا: فهي مجرد تحذيرات حول محتوى الملف. يعيد linter التشخيص. سنقوم بعمل شيء مماثل.
سنصنع أداة لإعلامنا بالكلمات التي نرغب في تجنبها. بعد ذلك ، سنوفر هذه الوظيفة لاثنين من برامج تحرير النصوص المختلفة.
خادم اللغة
أولا ، الأداة. سنخبز هذا الحق في خادم اللغة. للتبسيط ، سيكون هذا تطبيق Node.js ، على الرغم من أنه يمكننا القيام بذلك بأي تقنية قادرة على استخدام التدفقات للقراءة والكتابة.
هنا هو المنطق. بالنظر إلى بعض النصوص ، تُرجع هذه الطريقة مصفوفة من الكلمات المطابقة في القائمة السوداء والمؤشرات التي تم العثور عليها فيها.
const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results }
الآن ، لنجعله خادمًا.
const { TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.listen(connection) connection.listen()
هنا ، نحن vscode-languageserver
. الاسم مضلل ، حيث يمكن أن يعمل خارج VSCode بالتأكيد. هذه واحدة من "بصمات الأصابع" العديدة التي تراها لأصول LSP. vscode-languageserver
ببروتوكول المستوى الأدنى ويسمح لك بالتركيز على حالات الاستخدام. يبدأ هذا المقتطف اتصالاً ويربطه بمدير المستندات. عندما يتصل العميل بالخادم ، سيخبره الخادم بأنه يرغب في أن يتم إعلامه بفتح المستندات النصية.
يمكن أن نتوقف هنا. هذا خادم LSP يعمل بكامل طاقته ، وإن كان لا طائل من ورائه. بدلاً من ذلك ، دعنا نستجيب لتوثيق التغييرات ببعض المعلومات التشخيصية.
documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) })
أخيرًا ، نقوم بتوصيل النقاط بين المستند الذي تم تغييره ومنطقنا واستجابة التشخيص.

const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const { DiagnosticSeverity, } = require('vscode-languageserver') const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', })
ستكون حمولة التشخيص لدينا نتيجة تشغيل نص المستند من خلال وظيفتنا ، ثم تعيينه إلى التنسيق الذي يتوقعه العميل.
هذا البرنامج النصي سيخلق لك كل ذلك.
curl -o- https://raw.githubusercontent.com/reergymerej/lsp-article-resources/revision-for-6.0.0/blacklist-server-install.sh | bash
ملاحظة: إذا كنت غير مرتاح مع الغرباء الذين يضيفون ملفات تنفيذية إلى جهازك ، فيرجى التحقق من المصدر. يقوم بإنشاء المشروع وتنزيله index.js
و npm link
s لك.
مصدر الخادم الكامل
المصدر النهائي blacklist-server
هو:
#!/usr/bin/env node const { DiagnosticSeverity, TextDocuments, createConnection, } = require('vscode-languageserver') const {TextDocument} = require('vscode-languageserver-textdocument') const getBlacklisted = (text) => { const blacklist = [ 'foo', 'bar', 'baz', ] const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi') const results = [] while ((matches = regex.exec(text)) && results.length < 100) { results.push({ value: matches[0], index: matches.index, }) } return results } const blacklistToDiagnostic = (textDocument) => ({ index, value }) => ({ severity: DiagnosticSeverity.Warning, range: { start: textDocument.positionAt(index), end: textDocument.positionAt(index + value.length), }, message: `${value} is blacklisted.`, source: 'Blacklister', }) const getDiagnostics = (textDocument) => getBlacklisted(textDocument.getText()) .map(blacklistToDiagnostic(textDocument)) const connection = createConnection() const documents = new TextDocuments(TextDocument) connection.onInitialize(() => ({ capabilities: { textDocumentSync: documents.syncKind, }, })) documents.onDidChangeContent(change => { connection.sendDiagnostics({ uri: change.document.uri, diagnostics: getDiagnostics(change.document), }) }) documents.listen(connection) connection.listen()
دروس بروتوكول خادم اللغة: حان وقت اختبار القيادة
بعد تعديل link
المشروع ، حاول تشغيل الخادم ، مع تحديد stdio
كآلية النقل:
blacklist-server --stdio
إنه يستمع على stdio
الآن لرسائل LSP التي تحدثنا عنها من قبل. يمكننا توفيرها يدويًا ، لكن دعنا ننشئ عميلًا بدلاً من ذلك.
عميل اللغة: VSCode
نظرًا لأن هذه التقنية نشأت في VSCode ، يبدو من المناسب البدء من هناك. سننشئ امتدادًا من شأنه إنشاء عميل LSP وربطه بالخادم الذي أنشأناه للتو.
هناك عدد من الطرق لإنشاء امتداد VSCode ، بما في ذلك استخدام Yeoman والمولد المناسب ، generator-code
. من أجل التبسيط ، على الرغم من ذلك ، فلنقم بمثال مجردة.
لنستنسخ النموذج المعياري ونثبت تبعياته:
git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn
افتح دليل blacklist-vscode
في VSCode.
اضغط على F5 لبدء مثيل VSCode آخر ، مع تصحيح أخطاء الامتداد.
في "وحدة تصحيح الأخطاء" لمثيل VSCode ، سترى النص ، "انظر ، أماه. امتداد!"
لدينا الآن امتداد VSCode أساسي يعمل بدون كل الأجراس والصفارات. لنجعله عميل LSP. أغلق مثيلات VSCode ومن داخل دليل blacklist-vscode
، قم بتشغيل:
npm i vscode-languageclient
استبدل extension.js بـ:
const { LanguageClient } = require('vscode-languageclient') module.exports = { activate(context) { const executable = { command: 'blacklist-server', args: ['--stdio'], } const serverOptions = { run: executable, debug: executable, } const clientOptions = { documentSelector: [{ scheme: 'file', language: 'plaintext', }], } const client = new LanguageClient( 'blacklist-extension-id', 'Blacklister', serverOptions, clientOptions ) context.subscriptions.push(client.start()) }, }
يستخدم هذا الحزمة vscode-languageclient
لإنشاء عميل LSP داخل VSCode. على عكس vscode-languageserver
، هذا مقترن بإحكام بـ VSCode. باختصار ، ما نقوم به في هذا الامتداد هو إنشاء عميل وإخباره باستخدام الخادم الذي أنشأناه في الخطوات السابقة. بالتمتع بخصائص امتداد VSCode ، يمكننا أن نرى أننا نطلب منه استخدام عميل LSP هذا لملفات النص العادي.
لاختبار قيادتها ، افتح دليل blacklist-vscode
في VSCode. اضغط على F5 لبدء مثيل آخر ، مع تصحيح أخطاء الامتداد.
في مثيل VSCode الجديد ، أنشئ ملفًا نصيًا عاديًا واحفظه. اكتب "foo" أو "bar" وانتظر لحظة. سترى تحذيرات من أن هذه مدرجة في القائمة السوداء.
هذا هو! لم يكن علينا إعادة إنشاء أي من منطقنا ، فقط قم بالتنسيق بين العميل والخادم.
لنفعل ذلك مرة أخرى لمحرر آخر ، هذه المرة Sublime Text 3. ستكون العملية متشابهة تمامًا وأسهل قليلاً.
عميل اللغة: نص سامي 3
أولاً ، افتح ST3 وافتح لوحة الأوامر. نحتاج إلى إطار عمل لجعل المحرر عميل LSP. اكتب "Package Control: Install Package" واضغط على Enter. ابحث عن الحزمة "LSP" وقم بتثبيتها. بمجرد الانتهاء ، لدينا القدرة على تحديد عملاء LSP. هناك العديد من الإعدادات المسبقة ، لكننا لن نستخدمها. لقد أنشأنا منطقتنا.
مرة أخرى ، افتح لوحة الأوامر. ابحث عن "التفضيلات: إعدادات LSP" واضغط على إدخال. سيؤدي هذا إلى فتح ملف التكوين ، LSP.sublime-settings
، لحزمة LSP. لإضافة عميل مخصص ، استخدم التكوين أدناه.
{ "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }
قد يبدو هذا مألوفًا من امتداد VSCode. قمنا بتعريف العميل ، وطلبنا منه العمل على ملفات نصية عادية ، وحددنا خادم اللغة.
احفظ الإعدادات ، ثم أنشئ ملفًا نصيًا واحفظه. اكتب "foo" أو "bar" وانتظر. مرة أخرى ، سترى تحذيرات من إدراجها في القائمة السوداء. تختلف المعاملة - كيفية عرض الرسائل في المحرر. ومع ذلك ، فإن وظائفنا هي نفسها. بالكاد فعلنا أي شيء هذه المرة لإضافة دعم للمحرر.
لغة "العميل": Vim
إذا كنت لا تزال غير مقتنع بأن فصل الاهتمامات هذا يجعل من السهل مشاركة الميزات عبر برامج تحرير النصوص ، فإليك الخطوات لإضافة نفس الوظيفة إلى Vim عبر Coc.
افتح Vim واكتب :CocConfig
، ثم أضف:
"languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }
منجز.
يتيح الفصل بين الخادم والعميل ازدهار خدمات اللغات واللغات
من الواضح أن فصل مسؤولية خدمات اللغة عن محرري النصوص التي يتم استخدامها فيها يعد فوزًا. يسمح لمنشئي ميزات اللغة بالتركيز على تخصصهم ومنشئي المحرر للقيام بالمثل. إنها فكرة جديدة إلى حد ما ، لكن التبني آخذ في الانتشار.
الآن بعد أن أصبح لديك أساس للعمل منه ، ربما يمكنك العثور على مشروع والمساعدة في دفع هذه الفكرة إلى الأمام. لن تنتهي حرب شعلة المحرر أبدًا ، لكن لا بأس بذلك. طالما أن القدرات اللغوية يمكن أن توجد خارج محررين معينين ، فأنت حر في استخدام أي محرر تريده.
بصفتك شريكًا ذهبيًا لـ Microsoft ، فإن Toptal هي شبكة النخبة الخاصة بك من خبراء Microsoft. كوِّن فرقًا عالية الأداء مع الخبراء الذين تحتاجهم - في أي مكان وفي أي وقت بالضبط تحتاجهم!