언어 서버 프로토콜 구현

우리가 여기 어떻게 왔는지



2021년이 끝나기 전에 내 작업은 내 조직을 위한 해커톤을 예약했습니다. 요구 사항은 우리 또는 플레이어에게 유용해야 한다는 것입니다. 엔지니어가 생산 스택에 서비스를 제공할 수 있도록 language server을 개발하기로 결정했습니다.

우리의 스택은 예쁘고complex 많은 학습이 필요합니다. 상호 작용하는 언어는 yaml이지만 실제로는 DSL이며 많은 지원이 필요합니다. 이 도구의 목표는 스택에 익숙하지 않거나 일반적으로 익숙하지 않은 사람이 필요로 하는 지원의 양을 줄이는 데 도움을 주는 것이었습니다.

시작하고 실행하는 것이 꽤 어려웠기 때문에 다른 개발자 생산성 열광자와 내 작업 및 진행 상황을 공유하고 싶었습니다!

언어 서버란?



언어 서버는 언어 서버와 언어 클라이언트를 정의하는 Microsoft에서 개발한 표준입니다. 언어 구문 구문 분석, linting, 코드 힌트(등) 및 개발자와의 실제 인터페이스를 각각 처리합니다. 두 가지 언어 서버 클라이언트는 Visual Studio Code와 최근 버전 0.6의 Neovim입니다(전체가 Lua!로 작성됨). 일부 언어 서버에는 yamlls , sourcegraph-gorust-analyzer 가 포함됩니다. 그런 다음 언어 서버 프로토콜은 이 두 구성 요소 사이에 glue을 정의합니다. 접착제는 JSON-rcp를 통해 발생합니다.

맞춤형 LSP의 경우 yaml-language-server maintained by redhat 을 분기하여 시작합니다.

시작하기



가장 먼저 해야 할 일은 선택한 편집기에 대해 LSP를 활성화하는 것입니다. 제가 vim 진영에 푹 빠져서 네오빔을 했습니다. 이것은 yamlls를 실행하는 데 필요한 일반적인 설정 코드입니다.

local nvim_lsp = require('lspconfig')
local servers = { 
  'yamlls',  
}
for _, lsp in ipairs(servers) do
  nvim_lsp[lsp].setup {
    on_attach = on_attach,
    flags = {
      debounce_text_changes = 150,
    }
  }
end


언어 서버와 대화하도록 클라이언트를 설정하도록 neovim LSP에 지시합니다. yaml-language-server는 로컬 컴퓨터에서 바이너리를 실행합니다. 기본값은 --stdio 플래그로, 표준 입력을 받아들이고 유효성을 검사합니다: yaml-language-server --stdio . vim에서 :LspInfo를 실행하면 이 정보가 표시됩니다.

1 client(s) attached to this buffer: yamlls

  Client: yamlls (id 1)
    root:      /Users/ischweer/dev/shards
    filetypes: yaml
    cmd:       yaml-language-server --stdio


설정할 수 있는 구성이 많은 경향이 있습니다. 한 창에서 언어 서버를 실행하도록 선택한 경우 --rpc-port--rpc-host 옵션을 사용하여 언어 서버를 별도로 실행하고 다른 창에서 테스트할 수도 있습니다. .

내가 만든 것의 작은 데모



개발을 시작할 준비가 되었습니다. 제 경우에는 yaml-language-server를 포크하고 중앙 서비스에서 애플리케이션 사양, 해당 구성 및 WAN 설정을 가져오는 몇 가지 추가 논리를 추가하기 시작했습니다. Riot의 서비스에는 더 많은 비즈니스 DSL이 포함된 이러한 종류의 kube-esque 정의가 있습니다.

application-instance:
  name: some.app
  network:
    inbound:
      - name: another.app
        location: usw2


http를 통해 이들 모두를 아래로 당기는 것은 매우 간단합니다. 모든 yaml이 있으면 캐시됩니다.

  async downloadApps() {
    // we want to go through all the apps in the env, and get
    // the yaml'd app definitions
    await this.getDiscoverous();

    // now we can get the lol-services 710e env
    const url = `https://${this.gandalf_host}/api/v1/environments/lol-services/${env.LATEST_VERSION}`;
    const resp: XHRResponse = await xhr({ url: url, headers: { authorization: `Bearer ${this.gandalf_token}` } });
    console.log('Successfully grab 710e instance');

    const cache_builder: Array<Promise<boolean>> = [];

    for (const appMetadata of environmentInstance['environment']['applications']) {
      cache_builder.push(this.downloadApp(appMetadata));
    }

    await Promise.all(cache_builder);
  }

  async downloadApp(app: { name: string; version: string }): Promise<any> {
    const url = `https://${this.gandalf_host}/api/v1/applications/${app.name}/${app.version}`;
    const resp = await xhr({ url: url, headers: { authorization: `Bearer ${this.gandalf_token}` } });
    const _d = new YAML.Document();
    _d.contents = JSON.parse(resp.responseText);

    return new Promise(() => {
      fsp
        .writeFile(`${homedir()}/.cache/gandalf/${app.name}.yaml`, _d.toString(), { flag: 'wx' })
        .then()
        .catch((err) => {
          console.log(`Did not write file because it exists already ${app.name}.yaml`);
        });
    });
  }


이 yaml 구문 분석을 완전히 이해하려면 riot eng 블로그의 후속 블로그 게시물을 읽어야 합니다. 서비스 Y에 지정된 대화입니까, 아니면 오타입니까?". 타이프 스크립트에서 이렇게 보입니다.

    for (const defined_outbound of appSpec.outbounds || []) {
      let found = false;
      for (const _instanced_outbound of app_instance_outbounds.items || []) {
        const instanced_outbound = _instanced_outbound as YAMLMap;
        if (instanced_outbound.get('service') == defined_outbound) {
          found = true;
          break;
        }
      }
      if (!found) {
        errors.push({
          message: `Missing required outbound for ${defined_outbound}`,
          location: { start: app_instance_outbounds.range[0], end: app_instance_outbounds.range[2], toLineEnd: true },
          severity: 1,
          source: 'Gandalf',
          code: ErrorCode.Undefined,
        } as YAMLDocDiagnostic);
      }
    }


이를 통해 서비스가 서로 통신할 것으로 예상되지만 그렇게 지정되지 않은 경우 편집기에서 멋진 작은 오류를 볼 수 있습니다.



실제로 일을 하고



LSP는 RPC 전체에 걸쳐 있으며 위에서 지정한 모든 이벤트를 볼 수 있습니다. 모든 것은 비동기식이며 클라이언트는 모든 것에 대해 json 직렬화된 텍스트를 처리할 수 있어야 합니다. LSP가 어떻게 작동하는지 완전히 이해하려면 편집자 내부를 배워야 합니다 :').

neovim의 경우 항상 lsp 로그를 확인하십시오: tail -n 10000 ~/.cache/nvim/lsp.log | bat . 대부분의 경우 직렬화 오류 또는 알 수 없는 속성 문제가 있으므로 매우 유용합니다. 디버깅할 때 다음과 같은 "항상 기록"접근 방식을 추가하는 것이 도움이 된다는 것을 발견했습니다(편집기 속도가 느려지지만).

console.error = (arg) => {
  if (arg === null) {
    connection.console.info(arg);
  } else {
    connection.console.error(arg);
  }
};



이렇게 하면 더 일반적인 타이프스크립트 코드를 작성하고 로그에서 결과 오류를 읽은 다음 약간 더 긴밀한 피드백 루프를 가질 수 있습니다.

좋은 웹페이지 즐겨찾기