본문으로 건너뛰기

VSCode 정의 로 이동 내부 구현_VSCode 플러그인 개발 노트 4

무료2018-12-29#Node#VSCode Go to Definition#VSCode and tsserver#VSCode跳转原理#VSCode TSServer#VSCode TSServer plugin

퐁, 이동했습니다. Go to Definition 은 도대체 어떻게 된 일인가?

앞에 쓰는 말

[소스 코드](/articles/vs-code 源码简析/#articleHeader4) 로부터 보면, VSCode 본체는 단지 Editor 일 뿐입니다 (코어 부분은 Web 환경에서 독립적으로 실행 가능하며, Monaco 라고 불립니다). 언어 특성 관련 기능은 일절 제공하지 않습니다. 예를 들어:

  • 구문 서포트: 구문 검증, 하이라이트, 포맷, Lint 검사 등

  • 편집 체험: 정의 로 이동, 스마트 힌트, 자동 완성, 참조 검색, 변수 리네임 등

이들 모두 없으며, 모두 플러그인에 의해 제공되며, JS 에 대한 서포트도 마찬가지입니다

一.내장 플러그인

VS Code 내장 플러그인 중에서, JavaScript 와 관련된 것은 vscode/extensions/javascript/ 뿐이며, 게다가 순수한 언어 서포트형 플러그인입니다:

"contributes": {
  // 언어 id
  "languages": [],
  // 구문
  "grammars": [],
  // 코드 스니펫
  "snippets": [],
  // 언어 관련 설정 파일 검증 룰 및 힌트
  "jsonValidation": []
}

P.S.jsonValidation 의 작용에 대해서는, Json Schema with VS Code 참조

一堆의 설정 파일로는 명확히 정의 로 이동 등의 강력한 기능을 제공할 수 없으므로, 더욱 2 개의 TypeScript 관련 플러그인이 있습니다:

  • typescript-basics: javascript 플러그인과 유사하며, TS 언어 구문 서포트를 제공

  • typescript-language-features: 언어 특성 관련의 고도 서포트를 제공. 이동, 선언/참조 검색, 완성 힌트, outline/breadcrumb 등 코드 의미에 관련된 고도 기능

그 중에서 typescript-language-features 는 VS Code 가 JS/TS(및 JSX/TSX) 코드의 의미를 이해하고, 정의 로 이동 등의 기능을 서포트할 수 있는열쇠입니다:

"activationEvents": [
  "onLanguage:javascript",
  "onLanguage:javascriptreact",
  "onLanguage:typescript",
  "onLanguage:typescriptreact",
  "onLanguage:jsx-tags",
  "onLanguage:jsonc"
]

二.typescript-language-features

구조

./src
├── commands.ts   # TS 관련 커스텀 command
├── extension.ts  # 플러그인 엔트리
├── features  # 각종 언어 특성, 하이라이트, 접기, 정의 로 이동 등
├── languageProvider.ts # VSCode 기능 엔트리에对接
├── protocol.const.ts   # TS 언어 요소 상수
├── protocol.d.ts # tsserver 인터페이스 프로토콜
├── server.ts     # tsserver 프로세스를 관리
├── test
├── typeScriptServiceClientHost.ts  # Client 관리를 담당
├── typescriptService.ts        # Client 인터페이스 형태를 정의
├── typescriptServiceClient.ts  # Client 구체 실장
├── typings
└── utils

P.S.참조 소스 코드 버전 v1.28.2, 최신의 소스 코드 디렉토리 구조는 이미 변했지만,思路는 같습니다

그 중에서 가장 중요한 3 부분은 features, servertypescriptServiceClient 입니다:

  • Feature: VSCode 에对接하고, 하이라이트, 접기, 이동 등 Editor 기능 엔트리에 구체 실장을 제공

  • Server: TSServer 에接入하여, JS 코드 의미를 이해하는 능력을 획득하고, 의미 관련 기능에 데이터 소스를 제공

  • Client: Server 와 대화 (기정의 인터페이스 프로토콜에 따름), 리퀘스트를 발기하고, 응답 데이터를 수신

기동 플로

구체적으로는, 해당 플러그인이 액티브될 때 주로 이 3 가지 일이 발생했습니다:

  1. 모든 플러그인이 추가한 TypeScriptServerPlugin 을 찾아내고, Client ready 후에 등록

  2. TypeScriptServiceClientHost 를 작성

  3. TypeScriptServiceClient 를 작성하고, 즉시 TSServer 프로세스를 작성

  4. LanguageProvider 를 작성하고, VSCode 기능 엔트리에对接하는 것을 담당

  5. TSServer ready 후, VSCode 와 TSServer 의 연결을 시작

  6. LanguageProvider 가 VSCode 각항 기능을 등록. 예를 들어 vscode.languages.registerCompletionItemProvider 로 완성 힌트에对接

  7. 즉시 진단 (구문 검증, 타입 검사 등) 을 트리거

그 중에서 비교적 재미있는 것은 TypeScriptServerPlugin 의 등록, TSServer 의 작성, 및 Client 와 Server 간의 통신입니다

TypeScriptServerPlugin 의 등록

TS v2.3.0+ 만 외부 Plugin 을 등록하며, 커맨드라인 파라미터를 통해传入:

if (apiVersion.gte(API.v230)) {
  const pluginPaths = this._pluginPathsProvider.getPluginPaths();

  if (plugins.length) {
    args.push('--globalPlugins', plugins.map(x => x.name).join(','));

    if (currentVersion.path === this._versionProvider.defaultVersion.path) {
      pluginPaths.push(...plugins.map(x => x.path));
    }
  }

  if (pluginPaths.length !== 0) {
    args.push('--pluginProbeLocations', pluginPaths.join(','));
  }
}

TSServer plugin API 는 TS v2.3.0 에서推出되었기 때문입니다:

TypeScript 2.3 officially makes a language server plugin API available. This API allows plugins to augment the regular editing experience that TypeScript already delivers. What all of this means is that you can get an enhanced editing experience for many different workloads.

즉, VSCode 의 우주급 JS 편집 체험은, 모두 하층의 TypeScript 에 의한 은택입니다:

One of TypeScript's goals is to deliver a state-of-the-art editing experience to the JavaScript world.

(Announcing TypeScript 2.3 에서 인용)

P.S.저버전 TS 의 상황이 존재하는 이유는, VSCode 가 외부 TS 의 사용을 허가 하고 있기 때문입니다 (내장的是 물론 고버전)

TSServer 의 작성

TSServer 는 단독의 Node 프로세스에서 실행:

public spawn(
  version: TypeScriptVersion,
  configuration: TypeScriptServiceConfiguration,
  pluginManager: PluginManager
): TypeScriptServer {
  const apiVersion = version.version || API.defaultVersion;

  const { args, cancellationPipeName, tsServerLogFile } = this.getTsServerArgs(configuration, version, pluginManager);

  // fork 一个 tsserver 프로세스
  // 내장의 TSServer 는 extensions/node_modules/typescript/lib/tsserver.js 에 위치
  const childProcess = electron.fork(version.tsServerPath, args, this.getForkOptions());

  return new TypeScriptServer(childProcess, tsServerLogFile, cancellationPipeName, this._logger, this._telemetryReporter, this._tracer);
}

그 중에서, electron.fork 는 네이티브 fork() 의 캡슐화로, Electron API 액세스를 제한:

import cp = require('child_process');

export function fork(modulePath, args, options): cp.ChildProcess {
  const newEnv = generatePatchedEnv(process.env, modulePath);
  return cp.fork(modulePath, args, {
    silent: true,
    cwd: options.cwd,
    env: newEnv,
    execArgv: options.execArgv
  });
}

네이티브 cp.fork() 와의 차이는 환경 변수의 Patch 에 있습니다:

function generatePatchedEnv(env: any, modulePath: string): any {
  const newEnv = Object.assign({}, env);

  newEnv['ELECTRON_RUN_AS_NODE'] = '1';
  newEnv['NODE_PATH'] = path.join(modulePath, '..', '..', '..');

  // Ensure we always have a PATH set
  newEnv['PATH'] = newEnv['PATH'] || process.env.PATH;

  return newEnv;
}

그 중에서 ELECTRON_RUN_AS_NODEElectron API 액세스를 제한하기 위해 사용:

ELECTRON_RUN_AS_NODE: Starts the process as a normal Node.js process.

주로UI 커스터마이즈 제한과 안전성을 고려한 것으로, 否则第三者 VSCode 플러그인은 [typescriptServerPlugins 확장점](/articles/typescriptserverplugin-vscode 플러그인 개발 노트 3/#articleHeader8) 을 통해 Electron API 에 액세스하고, UI 를 개찬할 수 있습니다

P.S.보통 플러그인이 소재하는 Node 프로세스에도 이 제한이 있습니다. 상세는 [四。프로세스 모델](/articles/api 注入機制及플러그인 기동 플로-vscode 플러그인 개발 노트 2/#articleHeader8) 참조

Client 와 Server 통신

TSServer 가 자프로세스에서 실행하고 있으므로, API 호출에는 크로스프로세스의 문제가 존재합니다. 따라서 TSServer 는 한 조의 JSON 프로토콜 protocol.d.ts 를 정의하고, 주로 API 명 및 메시지 포맷을 포함합니다:

// 커맨드
const enum CommandTypes {
    Definition = "definition",
    Format = "format",
    References = "references",
    // ...
}

// 기본 메시지 포맷
interface Message {
    seq: number;
    type: "request" | "response" | "event";
}

// 리퀘스트 메시지 포맷
interface Request extends Message {
    type: "request";
    command: string;
    arguments?: any;
}

// 응답 메시지 포맷
interface Response extends Message {
    type: "response";
    request_seq: number;
    success: boolean;
    command: string;
    message?: string;
    body?: any;
    metadata?: unknown;
}

표준 입력/출력을 통해 메시지를 수신/발신합니다. 상세는 Message format 참조:

tsserver listens on stdin and writes messages back to stdout.

P.S.프로세스 간 통신에 관한 상세 정보는, [1.stdin/stdout 을 통해 json 을传递](/articles/nodejs 프로세스 간 통신/#articleHeader8) 참조

三.TSServer

TSServer 는 TS 와 밀접 불가분으로, 그림과 같습니다:

TypeScript Architectural Overview

그 중에서, 가장 중요한 3 블록은:

  • 컴파일러 코어 (Core TypeScript Compiler)

    완전한 컴파일러를 실장. 詞法 분석, 타입 검증, 구문 분석, 코드 생성 등을 포함

  • 에디터面向의 언어 서비스 (Language Service)

    문 완성, API 힌트, 코드 포맷, 파일 내 이동, 배색, 브레이크 위치 검증 등을 제공.还有一些 더シーン化된 API. 예를 들어 증분 컴파일. 상세는 Standalone Server (tsserver) 참조

  • 독립 컴파일러 (Standalone Compiler (tsc))

    CLI 툴. 입력 파일에 대해 컴파일 변환을 수행하고, 파일에 출력

TSServer 는 독립의 프로세스 서비스 (Standalone Server (tsserver)) 로서, Compiler 와 Service 의 위에 한 층의 캡슐화를建立하고, JSON 프로토콜의 형태로 인터페이스를暴露합니다. 상세는 Using the Language Service API 참조

따라서, TSServer 는 tsc 의 완전한 능력을 가지며, 에디터面向의 언어 서비스 서포트도 있어, 에디터 백그라운드 프로세스 등의 애플리케이션シーン에 매우 적합합니다

四.정리

至此, 모두가明了해졌습니다. 가장 중요한의미 분석 능력 및 데이터 서포트는 하층 TSServer 로부터입니다. 따라서, 정의 로 이동의 대략적인 플로 는 이렇습니다:

  1. 사용자가 VSCode 인터페이스에서 Go to Definition 을 클릭

  2. 내장 플러그인 typescript-language-features 가 등록한 대응 Feature 실장을 트리거

  3. Feature 가 Client 를 통해 TSServer 에의 리퀘스트를 발기

  4. TSServer 가 관련 AST 를 검색하여 Definitions 를 찾아내고, 기정 프로토콜 포맷에 따라 출력

  5. Client 가 응답을 받아, 데이터를 꺼내고, Feature 에传递

  6. Feature 가 원시 데이터를 VSCode 표시에 필요한 포맷으로 변환

  7. VSCode 가 데이터를 취득하고, 커서를 Editor 지정 위치로 이동. 퐁, 이동했습니다

P.S.VSCode 중의其它 JS 의미 관련 기능도 이것과 유사하며, 모두 TSServer 가 서포트를 제공

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성