언어 서버 프로토콜 자습서: VSCode에서 Vim으로
게시 됨: 2022-03-11모든 작업의 주요 아티팩트는 대부분 일반 텍스트 파일입니다. 메모장을 사용하여 작성하지 않으시겠습니까?
구문 강조 표시 및 자동 서식 지정은 빙산의 일각에 불과합니다. Linting, 코드 완성 및 반자동 리팩토링은 어떻습니까? 이것들은 모두 "실제" 코드 편집기를 사용해야 하는 아주 좋은 이유입니다. 이것들은 우리의 일상에 필수적이지만 그것들이 어떻게 작동하는지 이해하고 있습니까?
이 Language Server Protocol 튜토리얼에서 우리는 이러한 질문을 조금 탐구하고 텍스트 편집기가 작동하는 이유를 알아낼 것입니다. 결국 우리는 VSCode, Sublime Text 3 및 Vim용 예제 클라이언트와 함께 기본 언어 서버를 함께 구현할 것입니다.
컴파일러 대 언어 서비스
지금은 구문 강조 표시 및 서식 지정을 건너뛰고 정적 분석(그 자체로 흥미로운 주제)으로 처리되며 이러한 도구에서 얻는 주요 피드백에 중점을 둡니다. 컴파일러와 언어 서비스의 두 가지 주요 범주가 있습니다.
컴파일러는 소스 코드를 가져와서 다른 형식으로 내보냅니다. 코드가 언어의 규칙을 따르지 않으면 컴파일러는 오류를 반환합니다. 이것들은 꽤 친숙합니다. 이것의 문제는 일반적으로 매우 느리고 범위가 제한적이라는 것입니다. 코드를 생성하는 동안 도움을 제공하는 것은 어떻습니까?
이것이 언어 서비스가 제공하는 것입니다. 아직 작업 중인 동안 코드베이스에 대한 통찰력을 제공할 수 있으며 전체 프로젝트를 컴파일하는 것보다 훨씬 빠릅니다.
이러한 서비스의 범위는 다양합니다. 프로젝트의 모든 기호 목록을 반환하는 것과 같이 간단한 것일 수도 있고 코드를 리팩터링하는 단계를 반환하는 것과 같은 복잡한 것일 수도 있습니다. 이러한 서비스는 우리가 코드 편집기를 사용하는 주된 이유입니다. 컴파일하고 오류를 확인하려는 경우 몇 번의 키 입력으로 이를 수행할 수 있습니다. 언어 서비스는 더 많은 통찰력을 매우 빠르게 제공합니다.
프로그래밍을 위한 텍스트 편집기에 베팅하기
아직 특정 텍스트 편집기를 호출하지 않았습니다. 그 이유를 예를 들어 설명하겠습니다.
Lapine이라는 새로운 프로그래밍 언어를 개발했다고 가정해 보겠습니다. 그것은 아름다운 언어이며 컴파일러는 Elm과 같은 훌륭한 오류 메시지를 제공합니다. 또한 코드 완성, 참조, 리팩토링 도움말 및 진단을 제공할 수 있습니다.
어떤 코드/텍스트 편집기를 먼저 지원합니까? 그 후에는 어떨까요? 당신은 사람들이 그것을 채택하도록 하기 위해 힘든 싸움을 하고 있습니다. 그래서 당신은 그것을 가능한 한 쉽게 만들고 싶습니다. 잘못된 편집기를 선택하여 사용자를 놓치고 싶지 않습니다. 코드 편집기와 거리를 두고 전문 분야인 언어와 기능에 집중한다면 어떻게 될까요?
언어 서버
언어 서버 를 입력하십시오. 이들은 언어 클라이언트 와 대화하고 우리가 언급한 통찰력을 제공하는 도구입니다. 그들은 우리가 가상 상황에서 설명한 이유 때문에 텍스트 편집기와 독립적입니다.
평소와 같이 추상화의 또 다른 레이어가 필요한 것입니다. 이는 언어 도구와 코드 편집기의 긴밀한 결합을 깨뜨릴 것을 약속합니다. 언어 작성자는 서버에서 기능을 한 번 래핑할 수 있으며 코드/텍스트 편집기는 작은 확장을 추가하여 스스로를 클라이언트로 만들 수 있습니다. 모두에게 승리입니다. 그러나 이를 용이하게 하려면 이러한 클라이언트와 서버가 통신하는 방법에 동의해야 합니다.
다행히도 이것은 가설이 아닙니다. Microsoft는 이미 언어 서버 프로토콜을 정의하기 시작했습니다.
대부분의 위대한 아이디어와 마찬가지로, 그것은 선견지명이 아니라 필요에 의해 성장했습니다. 많은 코드 편집기가 이미 다양한 언어 기능에 대한 지원을 추가하기 시작했습니다. 일부 기능은 타사 도구에 아웃소싱되고 일부는 에디터 내부에서 수행됩니다. 확장성 문제가 발생했고 Microsoft가 문제를 분할하는 데 앞장섰습니다. 예, Microsoft는 이러한 기능을 VSCode에 저장하는 대신 코드 편집기에서 이동할 수 있는 방법을 마련했습니다. 그들은 계속해서 편집기를 구축하고 사용자를 가둘 수 있었지만 자유로워졌습니다.
언어 서버 프로토콜
언어 서버 프로토콜(LSP)은 언어 도구와 편집기를 분리하는 데 도움이 되도록 2016년에 정의되었습니다. 여전히 많은 VSCode 지문이 있지만 편집기 불가지론의 방향으로 가는 중요한 단계입니다. 프로토콜을 조금 살펴보겠습니다.
코드 편집기와 언어 도구를 생각하면 클라이언트와 서버는 간단한 문자 메시지로 통신합니다. 이러한 메시지에는 HTTP와 유사한 헤더, JSON-RPC 콘텐츠가 있으며 클라이언트 또는 서버에서 발생할 수 있습니다. JSON-RPC 프로토콜은 요청, 응답 및 알림과 이에 대한 몇 가지 기본 규칙을 정의합니다. 핵심 기능은 비동기식으로 작동하도록 설계되어 클라이언트/서버가 순서가 맞지 않고 병렬 처리를 통해 메시지를 처리할 수 있다는 것입니다.
간단히 말해서 JSON-RPC를 사용하면 클라이언트가 매개변수가 있는 메서드를 실행하고 결과 또는 오류를 반환하도록 다른 프로그램을 요청할 수 있습니다. LSP는 이를 기반으로 사용 가능한 방법, 예상되는 데이터 구조 및 트랜잭션에 대한 몇 가지 추가 규칙을 정의합니다. 예를 들어 클라이언트가 서버를 시작할 때 핸드셰이크 프로세스가 있습니다.
서버는 상태를 저장하며 한 번에 단일 클라이언트만 처리하도록 되어 있습니다. 그러나 통신에 대한 명시적인 제한은 없으므로 언어 서버는 클라이언트와 다른 시스템에서 실행할 수 있습니다. 그러나 실제로는 실시간 피드백의 경우 상당히 느릴 것입니다. 언어 서버와 클라이언트는 동일한 파일로 작업하며 꽤 수다스럽습니다.
LSP에는 무엇을 찾아야 하는지 알면 상당한 양의 문서가 있습니다. 언급했듯이 이 아이디어의 대부분은 VSCode의 컨텍스트 내에서 작성되지만 아이디어는 훨씬 더 광범위하게 적용됩니다. 예를 들어 프로토콜 사양은 모두 TypeScript로 작성됩니다. VSCode 및 TypeScript에 익숙하지 않은 탐험가를 돕기 위해 여기 입문서가 있습니다.
LSP 메시지 유형
언어 서버 프로토콜에 정의된 많은 메시지 그룹이 있습니다. 크게 "관리자"와 "언어 기능"으로 나눌 수 있습니다. 관리 메시지에는 클라이언트/서버 핸드셰이크, 파일 열기/변경 등에 사용되는 메시지가 포함됩니다. 중요한 것은 클라이언트와 서버가 처리하는 기능을 공유하는 곳이라는 점입니다. 확실히, 다른 언어와 도구는 다른 기능을 제공합니다. 이것은 또한 점진적인 채택을 허용합니다. Langserver.org는 클라이언트와 서버가 지원해야 하는 6가지 주요 기능의 이름을 지정하며 그 중 적어도 하나는 목록을 만드는 데 필요합니다.
언어 기능은 우리가 가장 관심을 갖고 있는 것입니다. 이 중 특별히 언급해야 할 것이 있습니다. 바로 진단 메시지입니다. 진단은 주요 기능 중 하나입니다. 파일을 열면 대부분 이것이 실행될 것이라고 가정합니다. 파일에 문제가 있으면 편집자가 알려줄 것입니다. 이것이 LSP에서 발생하는 방식은 다음과 같습니다.
- 클라이언트는 파일을 열고
textDocument/didOpen
을 서버에 보냅니다. - 서버는 파일을 분석하고
textDocument/publishDiagnostics
알림을 보냅니다. - 클라이언트는 결과를 구문 분석하고 편집기에 오류 표시기를 표시합니다.
이것은 언어 서비스에서 통찰력을 얻는 수동적인 방법입니다. 보다 적극적인 예는 커서 아래에 있는 기호에 대한 모든 참조를 찾는 것입니다. 이것은 다음과 같이 될 것입니다:
- 클라이언트는 파일의 위치를 지정하여
textDocument/references
를 서버에 보냅니다. - 서버는 기호를 파악하고 이 파일과 다른 파일에서 참조를 찾은 다음 목록으로 응답합니다.
- 클라이언트는 사용자에 대한 참조를 표시합니다.
블랙리스트 도구
우리는 언어 서버 프로토콜의 세부 사항을 확실히 파헤칠 수 있지만 클라이언트 구현자를 위해 남겨 두겠습니다. 편집기와 언어 도구 분리의 개념을 공고히 하기 위해 우리는 도구 작성자의 역할을 할 것입니다.
우리는 그것을 단순하게 유지하고 새로운 언어와 기능을 만드는 대신 진단에 충실할 것입니다. 진단은 적합합니다. 파일 내용에 대한 경고일 뿐입니다. 린터는 진단을 반환합니다. 우리는 비슷한 것을 만들 것입니다.
우리가 피하고 싶은 단어를 알려주는 도구를 만들 것입니다. 그런 다음 몇 가지 다른 텍스트 편집기에 해당 기능을 제공합니다.
언어 서버
첫째, 도구. 우리는 이것을 언어 서버에 바로 구울 것입니다. 단순함을 위해 이것은 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
합니다.
완전한 서버 소스
최종 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
우리가 이전에 이야기한 LSP 메시지에 대해 지금 stdio
에서 수신 대기 중입니다. 수동으로 제공할 수도 있지만 대신 클라이언트를 만들어 보겠습니다.
언어 클라이언트: VSCode
이 기술은 VSCode에서 시작되었으므로 여기에서 시작하는 것이 적절해 보입니다. LSP 클라이언트를 만들고 방금 만든 서버에 연결하는 확장을 만들 것입니다.
Yeoman 및 적절한 생성기인 generator-code
사용을 포함하여 VSCode 확장을 만드는 방법에는 여러 가지가 있습니다. 단순함을 위해 베어본 예를 들어보겠습니다.
상용구를 복제하고 종속성을 설치해 보겠습니다.
git clone [email protected]:reergymerej/standalone-vscode-ext.git blacklist-vscode cd blacklist-vscode npm i # or yarn
VSCode에서 blacklist-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
패키지를 사용하여 VSCode 내에서 LSP 클라이언트를 생성합니다. vscode-languageserver
와 달리 이것은 VSCode와 밀접하게 연결되어 있습니다. 간단히 말해서, 이 확장에서 우리가 하는 일은 클라이언트를 만들고 이전 단계에서 만든 서버를 사용하도록 지시하는 것입니다. VSCode 확장 기능을 자세히 살펴보면 일반 텍스트 파일에 이 LSP 클라이언트를 사용하도록 지시하고 있음을 알 수 있습니다.
테스트 드라이브를 하려면 VSCode에서 blacklist-vscode
디렉토리를 여십시오. F5 키를 눌러 확장을 디버깅하는 다른 인스턴스를 시작합니다.
새 VSCode 인스턴스에서 일반 텍스트 파일을 만들고 저장합니다. "foo" 또는 "bar"를 입력하고 잠시 기다리십시오. 블랙리스트에 있다는 경고가 표시됩니다.
그게 다야! 로직을 다시 만들 필요가 없었고 클라이언트와 서버를 조정하기만 하면 됩니다.
이번에는 Sublime Text 3인 다른 편집기를 위해 다시 해 보겠습니다. 프로세스는 매우 유사하고 조금 더 쉬울 것입니다.
언어 클라이언트: Sublime Text 3
먼저 ST3를 열고 명령 팔레트를 엽니다. 에디터를 LSP 클라이언트로 만들기 위한 프레임워크가 필요합니다. "패키지 제어: 패키지 설치"를 입력하고 Enter 키를 누릅니다. "LSP" 패키지를 찾아 설치합니다. 완료되면 LSP 클라이언트를 지정할 수 있습니다 . 많은 사전 설정이 있지만 우리는 그것들을 사용하지 않을 것입니다. 우리는 우리 자신을 만들었습니다.
다시 명령 팔레트를 엽니다. "기본 설정: LSP 설정"을 찾아 Enter 키를 누릅니다. 그러면 LSP 패키지에 대한 구성 파일 LSP.sublime-settings
가 열립니다. 사용자 지정 클라이언트를 추가하려면 아래 구성을 사용하세요.
{ "clients": { "blacklister": { "command": [ "blacklist-server", "--stdio" ], "enabled": true, "languages": [ { "syntaxes": [ "Plain text" ] } ] } }, "log_debug": true }
이것은 VSCode 확장에서 친숙해 보일 수 있습니다. 우리는 클라이언트를 정의하고 일반 텍스트 파일에서 작동하도록 지시했으며 언어 서버를 지정했습니다.
설정을 저장한 다음 일반 텍스트 파일을 만들어 저장합니다. "foo" 또는 "bar"를 입력하고 기다립니다. 다시, 당신은 이것들이 블랙리스트에 있다는 경고를 보게 될 것입니다. 메시지가 편집기에 표시되는 방식이 다릅니다. 그러나 우리의 기능은 동일합니다. 이번에는 에디터에 대한 지원을 추가하기 위해 거의 아무것도 하지 않았습니다.
언어 "클라이언트": Vim
이러한 문제의 분리가 텍스트 편집기 간에 기능을 쉽게 공유할 수 있다는 확신이 없다면 다음은 Coc을 통해 Vim에 동일한 기능을 추가하는 단계입니다.
Vim을 열고 :CocConfig
를 입력한 다음 다음을 추가합니다.
"languageserver": { "blacklister": { "command": "blacklist-server", "args": ["--stdio"], "filetypes": ["text"] } }
완료.
클라이언트-서버 분리로 언어 및 언어 서비스 번창
사용되는 텍스트 편집기에서 언어 서비스의 책임을 분리하는 것은 분명히 승리입니다. 이를 통해 언어 기능 작성자는 자신의 전문 분야에 집중하고 편집자 작성자는 동일한 작업을 수행할 수 있습니다. 상당히 새로운 아이디어지만 채택이 확산되고 있습니다.
이제 작업할 기반이 마련되었으므로 프로젝트를 찾고 이 아이디어를 발전시키는 데 도움이 될 수 있습니다. 편집기 불꽃 전쟁은 끝나지 않을 것이지만 괜찮습니다. 언어 능력이 특정 편집기 외부에 존재할 수 있는 한 원하는 편집기를 자유롭게 사용할 수 있습니다.
Microsoft 골드 파트너인 Toptal은 Microsoft 전문가로 구성된 엘리트 네트워크입니다. 언제 어디서나 필요한 전문가와 함께 고성능 팀을 구성하세요!