언어 서버 프로토콜

언어 서버 프로토콜이란?

편집기 또는 IDE의 프로그래밍 언어에 대한 원본 코드 자동 완성 또는 정의로 이동과 같은 풍부한 편집 기능을 지원하는 데는 일반적으로 매우 까다롭고 시간이 많이 걸립니다. 일반적으로 지원을 위해서는 편집기 또는 IDE의 프로그래밍 언어로 도메인 모델(스캐너, 파서, 형식 검사기, 작성기 등)을 작성해야 합니다. 예를 들어, Eclipse IDE 자체가 Java로 작성되었으므로 Eclipse IDE에서 C/C++를 지원하는 Eclipse CDT 플러그 인은 Java로 작성됩니다. 이 접근 방식에 따르면 Visual Studio Code용 TypeScript에서 C/C++ 도메인 모델을 구현하고 Visual Studio용 C#에서 별도의 도메인 모델을 구현합니다.

개발 도구에서 기존 언어별 라이브러리를 다시 사용할 수 있는 경우에도 언어별 도메인 모델을 만드는 것이 훨씬 더 쉽습니다. 그러나 이러한 라이브러리는 일반적으로 프로그래밍 언어 자체에서 구현됩니다(예: 좋은 C/C++ 도메인 모델은 C/C++에서 구현됨). C/C++ 라이브러리를 TypeScript로 작성된 편집기에 통합하는 것은 기술적으로 가능하지만 통합하기 어렵습니다.

언어 서버

또 다른 접근 방식은 자체 프로세스에서 라이브러리를 실행하고 프로세스 간 통신을 사용하여 통신하는 것입니다. 주고받는 메시지는 프로토콜을 형성합니다. LSP(언어 서버 프로토콜)는 개발 도구와 언어 서버 프로세스 간에 교환되는 메시지를 표준화하는 제품입니다. 언어 서버 또는 디먼을 사용하는 것은 새롭거나 참신한 아이디어가 아닙니다. Vim 및 Emacs와 같은 편집기는 의미 체계 자동 완성 지원을 제공하기 위해 한동안 이 작업을 수행했습니다. LSP의 목표는 이러한 종류의 통합을 간소화하고 다양한 도구에 언어 기능을 노출하는 데 유용한 프레임워크를 제공하는 것이었습니다.

공통 프로토콜을 사용하면 언어 도메인 모델의 기존 구현을 다시 사용하여 프로그래밍 언어 기능을 개발 도구에 통합할 수 있습니다. 언어 서버 백 엔드는 PHP, Python 또는 Java로 작성할 수 있으며 LSP를 사용하면 다양한 도구에 쉽게 통합할 수 있습니다. 프로토콜은 일반적인 추상화 수준에서 작동하므로 도구는 기본 도메인 모델과 관련된 뉘앙스를 완전히 이해하지 않고도 풍부한 언어 서비스를 제공할 수 있습니다.

LSP 작업의 시작 방식

LSP는 시간이 지남에 따라 진화했으며 현재 버전 3.0 상태입니다. C#에 대한 풍부한 편집 기능을 제공하기 위해 OmniSharp가 언어 서버의 개념을 선택했을 때 시작되었습니다. 처음에 OmniSharp은 JSON 페이로드와 함께 HTTP 프로토콜을 사용했으며 Visual Studio Code를 비롯한 여러 편집기에 통합되었습니다.

거의 동시에 Microsoft는 Emacs, Sublime Text 등의 편집기에서 TypeScript를 지원한다는 아이디어로 TypeScript 언어 서버 작업을 시작했습니다. 이 구현에서 편집기는 TypeScript 서버 프로세스에서 stdin/stdout을 통해 통신하고 요청 및 응답에 대해 V8 디버거 프로토콜에서 영감을 받은 JSON 페이로드를 사용합니다. TypeScript 서버는 풍부한 TypeScript 편집을 위해 TypeScript Sublime 플러그 인 및 VS Code에 통합되었습니다.

서로 다른 두 언어 서버를 통합한 후 VS Code 팀은 편집기 및 IDE에 대한 공통 언어 서버 프로토콜을 살펴보기 시작했습니다. 공통 프로토콜을 사용하면 언어 공급자가 다양한 IDE에서 사용할 수 있는 단일 언어 서버를 만들 수 있습니다. 언어 서버 소비자는 프로토콜의 클라이언트 쪽을 한 번만 구현해야 합니다. 이로 인해 언어 공급자와 언어 소비자 모두에게 이익이 되는 상황이 발생합니다.

언어 서버 프로토콜은 TypeScript 서버에서 사용하는 프로토콜로 시작하여 이 프로토콜을 VS Code 언어 API에서 영감을 받은 더 많은 언어 기능으로 확장했습니다. 이 프로토콜은 단순성 및 기존 라이브러리로 인해 원격 호출을 위한 JSON-RPC로 지원됩니다.

VS Code 팀은 파일을 lint(검사)하는 요청에 응답하고 검색된 경고 및 오류 세트를 반환하는 여러 Linter 언어 서버를 구현하여 프로토콜을 프로토타입화했습니다. 목표는 사용자가 문서에서 편집할 때 파일을 lint하는 것이었습니다. 즉, 편집기 세션 중에 많은 린팅 요청이 있을 것입니다. 각 사용자 편집에 대해 새 린팅 프로세스를 시작할 필요가 없도록 서버를 계속 실행하는 것이 합리적이었습니다. VS Code의 ESLint 및 TSLint 확장을 포함하여 여러 Linter 서버가 구현되었습니다. 이러한 두 Linter 서버는 모두 TypeScript/JavaScript에서 구현되고 Node.js에서 실행되었습니다. 두 서버는 프로토콜의 클라이언트 및 서버 부분을 구현하는 라이브러리를 공유합니다.

LSP 작동 방식

언어 서버는 자체 프로세스에서 실행되며 Visual Studio 또는 VS Code와 같은 도구는 JSON-RPC를 통해 언어 프로토콜을 사용하여 서버와 통신합니다. 전용 프로세스에서 작동하는 언어 서버의 또 다른 장점은 단일 프로세스 모델과 관련된 성능 문제가 방지된다는 것입니다. 클라이언트와 서버가 모두 Node.js로 작성된 경우 실제 전송 채널은 stdio, 소켓, 명명된 파이프 또는 노드 ipc일 수 있습니다.

다음은 루틴 편집 세션 중에 도구와 언어 서버가 통신하는 방법에 대한 예제입니다.

lsp flow diagram

  • 사용자가 도구에서 파일(문서라고 함)을 염: 도구는 문서가 열린다는 것(‘textDocument/didOpen’)을 언어 서버에 알립니다. 이제부터 문서의 콘텐츠는 더 이상 파일 시스템에 없지만 도구에 의해 메모리에 보관됩니다.

  • 사용자가 편집함: 도구는 문서 변경(‘textDocument/didChange’)에 대해 서버에 알리고 프로그램의 의미 체계 정보는 언어 서버에 의해 업데이트됩니다. 이 경우 언어 서버는 이 정보를 분석하고 검색된 오류와 경고(‘textDocument/publishDiagnostics’)에 관해 도구에 알립니다.

  • 사용자가 편집기의 기호에서 “정의로 이동”을 실행함: 도구는 두 개의 매개 변수인 (1) 문서 URI 및 (2) 정의로 이동 요청이 서버에 대해 시작된 위치를 사용하여 ‘textDocument/definition’ 요청을 보냅니다. 서버는 문서 URI 및 문서 내에서 기호 정의의 위치를 반환합니다.

  • 사용자가 문서(파일)를 닫음: 도구에서 ‘textDocument/didClose’ 알림이 전송되어 문서가 더 이상 메모리에 없으며 현재 콘텐츠가 파일 시스템에서 최신 상태임을 언어 서버에 알립니다.

이 예제에서는 “정의로 이동”, “모든 참조 찾기” 같은 편집기 기능 수준에서 프로토콜이 언어 서버와 통신하는 방법을 보여 줍니다. 프로토콜에서 사용하는 데이터 형식은 현재 열려 있는 텍스트 문서, 커서 위치 등의 편집기 또는 IDE ‘데이터 형식’입니다. 데이터 형식은 일반적으로 추상 구문 트리 및 컴파일러 기호(예: 확인된 형식, 네임스페이스, ...)를 제공하는 프로그래밍 언어 도메인 모델 수준에 있지 않습니다. 이로 인해 프로토콜이 크게 간소화됩니다.

이제 ‘textDocument/definition’ 요청을 더 자세히 살펴보겠습니다. 다음은 C++ 문서의 “정의로 이동” 요청을 위해 클라이언트 도구와 언어 서버 간에 이동하는 페이로드입니다.

요청은 다음과 같습니다.

{
    "jsonrpc": "2.0",
    "id" : 1,
    "method": "textDocument/definition",
    "params": {
        "textDocument": {
            "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp"
        },
        "position": {
            "line": 3,
            "character": 12
        }
    }
}

응답은 다음과 같습니다.

{
    "jsonrpc": "2.0",
    "id": "1",
    "result": {
        "uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp",
        "range": {
            "start": {
                "line": 0,
                "character": 4
            },
            "end": {
                "line": 0,
                "character": 11
            }
        }
    }
}

되돌아보면, 프로그래밍 언어 모델 수준이 아닌 편집기 수준에서 데이터 형식을 설명하는 것이 언어 서버 프로토콜의 성공 이유 중 하나입니다. 다양한 프로그래밍 언어 간에 추상 구문 트리 및 컴파일러 기호를 표준화하는 것에 비해 텍스트 문서 URI 또는 커서 위치를 표준화하는 것이 훨씬 더 간단합니다.

사용자가 다양한 언어로 작업하는 경우 VS Code는 일반적으로 프로그래밍 언어별로 언어 서버를 시작합니다. 아래 예제에서는 사용자가 Java 및 SASS 파일에서 작업하는 세션을 보여 줍니다.

java and sass

기능

모든 언어 서버가 프로토콜이 정의하는 모든 기능을 지원할 수 있는 것은 아닙니다. 따라서 클라이언트와 서버는 ‘기능’을 통해 지원되는 기능 세트를 알립니다. 예를 들어, 서버는 ‘textDocument/definition’ 요청을 처리할 수 있지만 ‘workspace/symbol’ 요청을 처리하지 못할 수 있음을 알립니다. 마찬가지로 클라이언트는 문서를 저장하기 전에 ‘저장하려고 합니다.’ 알림을 제공할 수 있으므로 서버가 텍스트 편집을 계산하여 편집된 문서의 서식을 자동으로 지정할 수 있음을 알릴 수 있습니다.

언어 서버 통합

언어 서버를 특정 도구에 실제로 통합하는 것은 언어 서버 프로토콜에서 정의하지 않으며 도구 구현자가 정의합니다. 일부 도구는 일반적으로 모든 종류의 언어 서버를 시작하고 통신할 수 있는 확장을 사용하여 언어 서버를 통합합니다. VS Code와 같은 다른 도구는 확장이 일부 사용자 지정 언어 기능을 계속 제공할 수 있도록 언어 서버별 사용자 지정 확장을 만듭니다.

언어 서버 및 클라이언트의 구현을 간소화하기 위해 클라이언트 및 서버 부분에 대한 라이브러리 또는 SDK가 있습니다. 이러한 라이브러리는 다양한 언어에 대해 제공됩니다. 예를 들어, 언어 서버를 VS Code 확장에 쉽게 통합할 수 있는 언어 클라이언트 npm 모듈 및 Node.js를 사용하여 언어 서버를 작성할 수 있는 또 다른 언어 서버 npm 모듈이 있습니다. 이는 지원 라이브러리의 현재 목록입니다.

Visual Studio에서 언어 서버 프로토콜 사용