이 출시되었습니다! 11월의 새로운 기능 및 수정 사항을 읽어보세요.

언어 서버 확장 프로그램 가이드

프로그래밍 가능한 언어 기능 주제에서 보셨듯이, languages.* API를 직접 사용하여 언어 기능을 구현할 수 있습니다. 그러나 언어 서버 확장은 이러한 언어 지원을 구현하는 대체 방법을 제공합니다.

이 주제에서는

  • 언어 서버 확장의 이점을 설명합니다.
  • Microsoft/vscode-languageserver-node 라이브러리를 사용하여 언어 서버를 빌드하는 과정을 안내합니다. lsp-sample의 코드로 바로 이동할 수도 있습니다.

언어 서버를 사용하는 이유는?

언어 서버는 많은 프로그래밍 언어의 편집 경험을 지원하는 특수한 종류의 Visual Studio Code 확장입니다. 언어 서버를 사용하면 VS Code에서 지원하는 자동 완성, 오류 검사(진단), 정의로 이동 등 다양한 언어 기능을 구현할 수 있습니다.

그러나 VS Code에서 언어 기능을 지원하면서 세 가지 일반적인 문제를 발견했습니다.

첫째, 언어 서버는 일반적으로 네이티브 프로그래밍 언어로 구현되며, 이는 Node.js 런타임을 사용하는 VS Code와 통합하는 데 어려움을 야기합니다.

또한 언어 기능은 리소스 집약적일 수 있습니다. 예를 들어, 파일을 올바르게 유효성 검사하려면 언어 서버는 많은 파일을 구문 분석하고, 추상 구문 트리를 구축하고, 정적 프로그램 분석을 수행해야 합니다. 이러한 작업은 상당한 CPU 및 메모리 사용을 유발할 수 있으며, VS Code의 성능에 영향을 미치지 않도록 해야 합니다.

마지막으로, 여러 프로그래밍 언어의 도구링을 여러 코드 편집기와 통합하는 데 상당한 노력이 필요할 수 있습니다. 언어 도구링의 관점에서는 다른 API를 가진 코드 편집기에 적응해야 합니다. 코드 편집기의 관점에서는 언어 도구링에서 일관된 API를 기대할 수 없습니다. 이로 인해 N개의 코드 편집기에서 M개의 언어에 대한 언어 지원을 구현하는 작업이 M * N이 됩니다.

이러한 문제를 해결하기 위해 Microsoft는 언어 도구링과 코드 편집기 간의 통신을 표준화하는 Language Server Protocol을 지정했습니다. 이를 통해 언어 서버는 어떤 언어로든 구현될 수 있으며 자체 프로세스에서 실행되어 성능 비용을 피할 수 있습니다. 언어 서버 프로토콜을 통해 코드 편집기와 통신하기 때문입니다. 또한, LSP를 준수하는 언어 도구링은 여러 LSP를 준수하는 코드 편집기와 통합할 수 있으며, LSP를 준수하는 코드 편집기는 여러 LSP를 준수하는 언어 도구링을 쉽게 사용할 수 있습니다. LSP는 언어 도구링 제공업체와 코드 편집기 공급업체 모두에게 이익입니다!

LSP Languages and Editors

이 가이드에서는

  • 제공된 Node SDK를 사용하여 VS Code에서 언어 서버 확장을 빌드하는 방법을 설명합니다.
  • 언어 서버 확장을 실행, 디버그, 로깅 및 테스트하는 방법을 설명합니다.
  • 언어 서버에 대한 고급 주제로 안내합니다.

언어 서버 구현

개요

VS Code에서 언어 서버는 두 가지 구성 요소로 이루어집니다.

  • 언어 클라이언트: JavaScript/TypeScript로 작성된 일반적인 VS Code 확장입니다. 이 확장은 모든 VS Code 네임스페이스 API에 액세스할 수 있습니다.
  • 언어 서버: 별도의 프로세스에서 실행되는 언어 분석 도구입니다.

앞서 간략히 언급했듯이 언어 서버를 별도의 프로세스에서 실행하는 데에는 두 가지 이점이 있습니다.

  • 분석 도구는 언어 서버 프로토콜을 따라 언어 클라이언트와 통신할 수 있는 한 어떤 언어로든 구현될 수 있습니다.
  • 언어 분석 도구는 종종 CPU 및 메모리 사용량이 많기 때문에 별도의 프로세스에서 실행하면 성능 비용을 피할 수 있습니다.

다음은 VS Code에서 두 개의 언어 서버 확장을 실행하는 예시입니다. HTML 언어 클라이언트와 PHP 언어 클라이언트는 TypeScript로 작성된 일반적인 VS Code 확장입니다. 각 확장은 해당 언어 서버를 인스턴스화하고 LSP를 통해 통신합니다. PHP 언어 서버는 PHP로 작성되었지만 LSP를 통해 PHP 언어 클라이언트와 통신할 수 있습니다.

LSP Illustration

이 가이드에서는 제공된 Node SDK를 사용하여 클라이언트/서버 언어 서버를 빌드하는 방법을 배웁니다. 나머지 문서는 VS Code 확장 API에 익숙하다고 가정합니다.

LSP 샘플 - 일반 텍스트 파일용 간단한 언어 서버

일반 텍스트 파일에 대한 자동 완성 및 진단을 구현하는 간단한 언어 서버 확장을 빌드해 보겠습니다. 또한 클라이언트/서버 간의 구성 동기화도 다룰 것입니다.

코드로 바로 이동하려면

  • lsp-sample: 이 가이드에 대한 자세한 문서가 포함된 소스 코드입니다.
  • lsp-multi-server-sample: VS Code의 멀티 루트 워크스페이스 기능을 지원하기 위해 워크스페이스 폴더당 다른 서버 인스턴스를 시작하는 lsp-sample의 고급 버전으로, 자세한 문서가 포함되어 있습니다.

Microsoft/vscode-extension-samples 리포지토리를 복제하고 샘플을 엽니다.

> git clone https://github.com/microsoft/vscode-extension-samples.git
> cd vscode-extension-samples/lsp-sample
> npm install
> npm run compile
> code .

위 단계는 모든 종속성을 설치하고 클라이언트 코드와 서버 코드를 모두 포함하는 **lsp-sample** 워크스페이스를 엽니다. **lsp-sample**의 구조에 대한 개요입니다.

.
├── client // Language Client
│   ├── src
│   │   ├── test // End to End tests for Language Client / Server
│   │   └── extension.ts // Language Client entry point
├── package.json // The extension manifest
└── server // Language Server
    └── src
        └── server.ts // Language Server entry point

'언어 클라이언트' 설명

먼저 언어 클라이언트의 기능을 설명하는 /package.json을 살펴보겠습니다. 흥미로운 섹션은 두 가지입니다.

먼저 configuration 섹션을 살펴보겠습니다.

"configuration": {
    "type": "object",
    "title": "Example configuration",
    "properties": {
        "languageServerExample.maxNumberOfProblems": {
            "scope": "resource",
            "type": "number",
            "default": 100,
            "description": "Controls the maximum number of problems produced by the server."
        }
    }
}

이 섹션은 VS Code에 configuration 설정을 제공합니다. 예시에서는 이러한 설정이 시작 시 언어 서버로 전송되는 방법과 설정이 변경될 때마다 전송되는 방법을 설명합니다.

참고: 확장이 VS Code 1.74.0 이전 버전을 지원하는 경우, /package.jsonactivationEvents 필드에 onLanguage:plaintext를 선언하여 일반 텍스트 파일(예: .txt 확장자를 가진 파일)이 열릴 때 확장이 활성화되도록 VS Code에 알려야 합니다.

"activationEvents": []

실제 언어 클라이언트 소스 코드와 해당 package.json/client 폴더에 있습니다. /client/package.json 파일의 흥미로운 부분은 engines 필드를 통해 vscode 확장 호스트 API를 참조하고 vscode-languageclient 라이브러리에 대한 종속성을 추가한다는 것입니다.

"engines": {
    "vscode": "^1.52.0"
},
"dependencies": {
    "vscode-languageclient": "^7.0.0"
}

언급했듯이 클라이언트는 일반 VS Code 확장으로 구현되며 모든 VS Code 네임스페이스 API에 액세스할 수 있습니다.

**lsp-sample** 확장 진입점인 해당 extension.ts 파일의 내용은 다음과 같습니다.

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';

import {
  LanguageClient,
  LanguageClientOptions,
  ServerOptions,
  TransportKind
} from 'vscode-languageclient/node';

let client: LanguageClient;

export function activate(context: ExtensionContext) {
  // The server is implemented in node
  let serverModule = context.asAbsolutePath(path.join('server', 'out', 'server.js'));
  // The debug options for the server
  // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
  let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };

  // If the extension is launched in debug mode then the debug server options are used
  // Otherwise the run options are used
  let serverOptions: ServerOptions = {
    run: { module: serverModule, transport: TransportKind.ipc },
    debug: {
      module: serverModule,
      transport: TransportKind.ipc,
      options: debugOptions
    }
  };

  // Options to control the language client
  let clientOptions: LanguageClientOptions = {
    // Register the server for plain text documents
    documentSelector: [{ scheme: 'file', language: 'plaintext' }],
    synchronize: {
      // Notify the server about file changes to '.clientrc files contained in the workspace
      fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
    }
  };

  // Create the language client and start the client.
  client = new LanguageClient(
    'languageServerExample',
    'Language Server Example',
    serverOptions,
    clientOptions
  );

  // Start the client. This will also launch the server
  client.start();
}

export function deactivate(): Thenable<void> | undefined {
  if (!client) {
    return undefined;
  }
  return client.stop();
}

'언어 서버' 설명

참고: GitHub 리포지토리에서 복제한 '서버' 구현은 최종 워크스루 구현입니다. 워크스루를 따르려면 새 server.ts 파일을 만들거나 복제한 버전의 내용을 수정할 수 있습니다.

이 예시에서는 서버도 TypeScript로 구현되었으며 Node.js를 사용하여 실행됩니다. VS Code에는 이미 Node.js 런타임이 포함되어 있으므로 특정 런타임 요구 사항이 없는 한 자체 런타임을 제공할 필요는 없습니다.

언어 서버의 소스 코드는 /server에 있습니다. 서버의 package.json 파일에서 흥미로운 섹션은 다음과 같습니다.

"dependencies": {
    "vscode-languageserver": "^7.0.0",
    "vscode-languageserver-textdocument": "^1.0.1"
}

이는 vscode-languageserver 라이브러리를 가져옵니다.

다음은 제공된 텍스트 문서 관리자를 사용하여 VS Code에서 서버로 점진적인 델타를 항상 전송하여 텍스트 문서를 동기화하는 서버 구현입니다.

import {
  createConnection,
  TextDocuments,
  Diagnostic,
  DiagnosticSeverity,
  ProposedFeatures,
  InitializeParams,
  DidChangeConfigurationNotification,
  CompletionItem,
  CompletionItemKind,
  TextDocumentPositionParams,
  TextDocumentSyncKind,
  InitializeResult
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

// Create a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
let connection = createConnection(ProposedFeatures.all);

// Create a simple text document manager.
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;

connection.onInitialize((params: InitializeParams) => {
  let capabilities = params.capabilities;

  // Does the client support the `workspace/configuration` request?
  // If not, we fall back using global settings.
  hasConfigurationCapability = !!(
    capabilities.workspace && !!capabilities.workspace.configuration
  );
  hasWorkspaceFolderCapability = !!(
    capabilities.workspace && !!capabilities.workspace.workspaceFolders
  );
  hasDiagnosticRelatedInformationCapability = !!(
    capabilities.textDocument &&
    capabilities.textDocument.publishDiagnostics &&
    capabilities.textDocument.publishDiagnostics.relatedInformation
  );

  const result: InitializeResult = {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      // Tell the client that this server supports code completion.
      completionProvider: {
        resolveProvider: true
      }
    }
  };
  if (hasWorkspaceFolderCapability) {
    result.capabilities.workspace = {
      workspaceFolders: {
        supported: true
      }
    };
  }
  return result;
});

connection.onInitialized(() => {
  if (hasConfigurationCapability) {
    // Register for all configuration changes.
    connection.client.register(DidChangeConfigurationNotification.type, undefined);
  }
  if (hasWorkspaceFolderCapability) {
    connection.workspace.onDidChangeWorkspaceFolders(_event => {
      connection.console.log('Workspace folder change event received.');
    });
  }
});

// The example settings
interface ExampleSettings {
  maxNumberOfProblems: number;
}

// The global settings, used when the `workspace/configuration` request is not supported by the client.
// Please note that this is not the case when using this server with the client provided in this example
// but could happen with other clients.
const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
let globalSettings: ExampleSettings = defaultSettings;

// Cache the settings of all open documents
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

// Only keep settings for open documents
documents.onDidClose(e => {
  documentSettings.delete(e.document.uri);
});

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
  validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

connection.onDidChangeWatchedFiles(_change => {
  // Monitored files have change in VS Code
  connection.console.log('We received a file change event');
});

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

// Listen on the connection
connection.listen();

간단한 유효성 검사 추가

문서 유효성 검사를 서버에 추가하기 위해 텍스트 문서 관리자에 리스너를 추가합니다. 이 리스너는 텍스트 문서 내용이 변경될 때마다 호출됩니다. 문서를 유효성 검사하기 가장 좋은 시기를 결정하는 것은 서버의 몫입니다. 예시 구현에서는 서버가 일반 텍스트 문서를 유효성 검사하고 모든 대문자 단어의 발생을 플래그합니다. 해당 코드 조각은 다음과 같습니다.

// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(async change => {
  let textDocument = change.document;
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
});

진단 팁 및 요령

  • 시작 및 끝 위치가 같으면 VS Code는 해당 위치의 단어를 물결선으로 밑줄 칩니다.
  • 줄 끝까지 물결선으로 밑줄을 긋고 싶다면 끝 위치의 문자를 Number.MAX_VALUE로 설정하십시오.

언어 서버를 실행하려면 다음 단계를 따르십시오.

  • 빌드 작업을 시작하려면 ⇧⌘B (Windows, Linux Ctrl+Shift+B)를 누릅니다. 이 작업은 클라이언트와 서버를 모두 컴파일합니다.
  • 실행 보기에서 **클라이언트 시작** 시작 구성을 선택하고 **디버그 시작** 버튼을 눌러 확장의 코드를 실행하는 추가 VS Code 인스턴스인 **확장 개발 호스트**를 시작합니다.
  • 루트 폴더에 test.txt 파일을 만들고 다음 내용을 붙여넣습니다.
TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY host. ANY OS. Open Source.

**확장 개발 호스트** 인스턴스는 다음과 같이 표시됩니다.

Validating a text file

클라이언트 및 서버 디버깅

클라이언트 코드를 디버깅하는 것은 일반 확장을 디버깅하는 것만큼 쉽습니다. 클라이언트 코드에 중단점을 설정하고 F5를 눌러 확장을 디버그합니다.

Debugging the client

서버는 확장(클라이언트)에서 실행되는 LanguageClient에 의해 시작되므로, 실행 중인 서버에 디버거를 연결해야 합니다. 이렇게 하려면 **실행 및 디버그** 보기로 전환하고 **서버에 연결** 시작 구성을 선택하고 F5를 누릅니다. 그러면 서버에 디버거가 연결됩니다.

Debugging the server

언어 서버를 위한 로깅 지원

클라이언트를 구현하기 위해 vscode-languageclient를 사용하는 경우, 클라이언트가 name 채널에 언어 클라이언트/서버 간 통신을 기록하도록 지시하는 [langId].trace.server 설정을 지정할 수 있습니다.

**lsp-sample**의 경우, 이 설정을 "languageServerExample.trace.server": "verbose"로 설정할 수 있습니다. 이제 "Language Server Example" 채널로 이동하면 로그가 표시됩니다.

LSP Log

서버에서 구성 설정 사용

확장의 클라이언트 부분을 작성할 때 보고할 문제의 최대 수를 제어하는 설정을 이미 정의했습니다. 클라이언트로부터 이러한 설정을 읽도록 서버 측 코드도 작성했습니다.

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
  if (!hasConfigurationCapability) {
    return Promise.resolve(globalSettings);
  }
  let result = documentSettings.get(resource);
  if (!result) {
    result = connection.workspace.getConfiguration({
      scopeUri: resource,
      section: 'languageServerExample'
    });
    documentSettings.set(resource, result);
  }
  return result;
}

이제 서버 측에서 구성 변경을 수신 대기하고 설정이 변경되면 열린 텍스트 문서를 다시 유효성 검사해야 합니다. 문서 변경 이벤트 처리의 유효성 검사 논리를 재사용하기 위해 코드를 validateTextDocument 함수로 추출하고 maxNumberOfProblems 변수를 존중하도록 코드를 수정합니다.

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
  // In this simple example we get the settings for every validate run.
  let settings = await getDocumentSettings(textDocument.uri);

  // The validator creates diagnostics for all uppercase words length 2 and more
  let text = textDocument.getText();
  let pattern = /\b[A-Z]{2,}\b/g;
  let m: RegExpExecArray | null;

  let problems = 0;
  let diagnostics: Diagnostic[] = [];
  while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
    problems++;
    let diagnostic: Diagnostic = {
      severity: DiagnosticSeverity.Warning,
      range: {
        start: textDocument.positionAt(m.index),
        end: textDocument.positionAt(m.index + m[0].length)
      },
      message: `${m[0]} is all uppercase.`,
      source: 'ex'
    };
    if (hasDiagnosticRelatedInformationCapability) {
      diagnostic.relatedInformation = [
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Spelling matters'
        },
        {
          location: {
            uri: textDocument.uri,
            range: Object.assign({}, diagnostic.range)
          },
          message: 'Particularly for names'
        }
      ];
    }
    diagnostics.push(diagnostic);
  }

  // Send the computed diagnostics to VS Code.
  connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

구성 변경 처리는 연결에 구성 변경에 대한 알림 핸들러를 추가하여 수행됩니다. 해당 코드는 다음과 같습니다.

connection.onDidChangeConfiguration(change => {
  if (hasConfigurationCapability) {
    // Reset all cached document settings
    documentSettings.clear();
  } else {
    globalSettings = <ExampleSettings>(
      (change.settings.languageServerExample || defaultSettings)
    );
  }

  // Revalidate all open text documents
  documents.all().forEach(validateTextDocument);
});

클라이언트를 다시 시작하고 설정을 최대 1개의 문제를 보고하도록 변경하면 다음과 같은 유효성 검사가 이루어집니다.

Maximum One Problem

추가 언어 기능 추가

언어 서버가 일반적으로 구현하는 첫 번째 흥미로운 기능은 문서 유효성 검사입니다. 이러한 의미에서 린터도 언어 서버로 간주되며 VS Code에서는 린터가 일반적으로 언어 서버로 구현됩니다(예: eslintjshint 참조). 그러나 언어 서버에는 더 많은 기능이 있습니다. 코드 완성, 모든 참조 찾기 또는 정의로 이동을 제공할 수 있습니다. 아래 예시 코드는 서버에 코드 완성을 추가합니다. 'TypeScript'와 'JavaScript' 두 단어를 제안합니다.

// This handler provides the initial list of the completion items.
connection.onCompletion(
  (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // The pass parameter contains the position of the text document in
    // which code complete got requested. For the example we ignore this
    // info and always provide the same completion items.
    return [
      {
        label: 'TypeScript',
        kind: CompletionItemKind.Text,
        data: 1
      },
      {
        label: 'JavaScript',
        kind: CompletionItemKind.Text,
        data: 2
      }
    ];
  }
);

// This handler resolves additional information for the item selected in
// the completion list.
connection.onCompletionResolve(
  (item: CompletionItem): CompletionItem => {
    if (item.data === 1) {
      item.detail = 'TypeScript details';
      item.documentation = 'TypeScript documentation';
    } else if (item.data === 2) {
      item.detail = 'JavaScript details';
      item.documentation = 'JavaScript documentation';
    }
    return item;
  }
);

data 필드는 해결 핸들러에서 완료 항목을 고유하게 식별하는 데 사용됩니다. data 속성은 프로토콜에 투명합니다. 기본 메시지 전달 프로토콜은 JSON 기반이므로 data 필드에는 JSON으로 직렬화/역직렬화할 수 있는 데이터만 포함해야 합니다.

이제 서버가 코드 완성 요청을 지원한다는 것을 VS Code에 알려야 합니다. 이렇게 하려면 초기화 핸들러에서 해당 기능을 플래그합니다.

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            ...
            // Tell the client that the server supports code completion
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});

아래 스크린샷은 일반 텍스트 파일에서 실행되는 완성된 코드입니다.

Code Complete

언어 서버 테스트

고품질 언어 서버를 만들기 위해서는 기능에 대한 좋은 테스트 제품군을 구축해야 합니다. 언어 서버를 테스트하는 데에는 두 가지 일반적인 방법이 있습니다.

  • 단위 테스트: 언어 서버의 특정 기능을 테스트하고 모든 정보를 모의할 때 유용합니다. VS Code의 HTML / CSS / JSON 언어 서버는 이 방식을 사용합니다. LSP npm 모듈도 이 방식을 사용합니다. npm 프로토콜 모듈을 사용하여 작성된 일부 단위 테스트는 여기에서 볼 수 있습니다.
  • End-to-End 테스트: VS Code 확장 테스트와 유사합니다. 이 접근 방식의 이점은 워크스페이스가 있는 VS Code 인스턴스를 인스턴스화하고, 파일을 열고, 언어 클라이언트/서버를 활성화하고, VS Code 명령을 실행하여 테스트를 실행한다는 것입니다. 이 접근 방식은 모의하기 어렵거나 불가능한 파일, 설정 또는 종속성(예: node_modules)이 있는 경우 더 우수합니다. 인기 있는 Python 확장은 이 방식을 사용하여 테스트합니다.

선택한 테스트 프레임워크에서 단위 테스트를 수행할 수 있습니다. 여기서는 언어 서버 확장에 대한 End-to-End 테스트 방법을 설명합니다.

.vscode/launch.json을 열면 E2E 테스트 대상을 찾을 수 있습니다.

{
  "name": "Language Server E2E Test",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceRoot}",
    "--extensionTestsPath=${workspaceRoot}/client/out/test/index",
    "${workspaceRoot}/client/testFixture"
  ],
  "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}

이 디버그 대상을 실행하면 client/testFixture를 활성 워크스페이스로 하는 VS Code 인스턴스가 시작됩니다. 그런 다음 VS Code는 client/src/test의 모든 테스트를 실행합니다. 디버깅 팁으로 client/src/test의 TypeScript 파일에 중단점을 설정하면 히트합니다.

completion.test.ts 파일을 살펴보겠습니다.

import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';

suite('Should do completion', () => {
  const docUri = getDocUri('completion.txt');

  test('Completes JS/TS in txt file', async () => {
    await testCompletion(docUri, new vscode.Position(0, 0), {
      items: [
        { label: 'JavaScript', kind: vscode.CompletionItemKind.Text },
        { label: 'TypeScript', kind: vscode.CompletionItemKind.Text }
      ]
    });
  });
});

async function testCompletion(
  docUri: vscode.Uri,
  position: vscode.Position,
  expectedCompletionList: vscode.CompletionList
) {
  await activate(docUri);

  // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion
  const actualCompletionList = (await vscode.commands.executeCommand(
    'vscode.executeCompletionItemProvider',
    docUri,
    position
  )) as vscode.CompletionList;

  assert.ok(actualCompletionList.items.length >= 2);
  expectedCompletionList.items.forEach((expectedItem, i) => {
    const actualItem = actualCompletionList.items[i];
    assert.equal(actualItem.label, expectedItem.label);
    assert.equal(actualItem.kind, expectedItem.kind);
  });
}

이 테스트에서는

  • 확장을 활성화합니다.
  • vscode.executeCompletionItemProvider 명령을 URI와 위치와 함께 실행하여 자동 완성 트리거를 시뮬레이션합니다.
  • 반환된 완료 항목을 예상 완료 항목과 비교하여 확인합니다.

activate(docURI) 함수를 좀 더 자세히 살펴보겠습니다. client/src/test/helper.ts에 정의되어 있습니다.

import * as vscode from 'vscode';
import * as path from 'path';

export let doc: vscode.TextDocument;
export let editor: vscode.TextEditor;
export let documentEol: string;
export let platformEol: string;

/**
 * Activates the vscode.lsp-sample extension
 */
export async function activate(docUri: vscode.Uri) {
  // The extensionId is `publisher.name` from package.json
  const ext = vscode.extensions.getExtension('vscode-samples.lsp-sample')!;
  await ext.activate();
  try {
    doc = await vscode.workspace.openTextDocument(docUri);
    editor = await vscode.window.showTextDocument(doc);
    await sleep(2000); // Wait for server activation
  } catch (e) {
    console.error(e);
  }
}

async function sleep(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

활성화 부분에서

  • package.json에 정의된 대로 {publisher.name}.{extensionId}를 사용하여 확장을 가져옵니다.
  • 지정된 문서를 열고 활성 텍스트 편집기에서 표시합니다.
  • 2초 동안 일시 중지하여 언어 서버가 인스턴스화되었는지 확인합니다.

준비 후, 각 언어 기능에 해당하는 VS Code 명령을 실행하고 반환된 결과와 비교할 수 있습니다.

방금 구현한 진단 기능을 다루는 테스트가 하나 더 있습니다. client/src/test/diagnostics.test.ts에서 확인해 보세요.

고급 주제

지금까지 이 가이드에서는 다음 내용을 다뤘습니다.

  • 언어 서버 및 언어 서버 프로토콜 개요.
  • VS Code의 언어 서버 확장 아키텍처
  • **lsp-sample** 확장 및 개발/디버그/검사/테스트 방법.

이 가이드에 포함되지 않은 더 고급 주제가 있습니다. 언어 서버 개발에 대한 추가 학습을 위해 이러한 리소스에 대한 링크를 포함하겠습니다.

추가 언어 서버 기능

다음 언어 기능은 현재 코드 완성 기능과 함께 언어 서버에서 지원됩니다.

  • 문서 하이라이트: 텍스트 문서에서 모든 '동일한' 기호를 강조 표시합니다.
  • 호버: 텍스트 문서에서 선택한 기호에 대한 호버 정보를 제공합니다.
  • 시그니처 도움말: 텍스트 문서에서 선택한 기호에 대한 시그니처 도움말을 제공합니다.
  • 정의로 이동: 텍스트 문서에서 선택한 기호에 대한 정의로 이동 지원을 제공합니다.
  • 유형 정의로 이동: 텍스트 문서에서 선택한 기호에 대한 유형/인터페이스 정의로 이동 지원을 제공합니다.
  • 구현으로 이동: 텍스트 문서에서 선택한 기호에 대한 구현 정의로 이동 지원을 제공합니다.
  • 참조 찾기: 텍스트 문서에서 선택한 기호에 대한 프로젝트 전체의 모든 참조를 찾습니다.
  • 문서 기호 목록: 텍스트 문서에 정의된 모든 기호를 나열합니다.
  • 워크스페이스 기호 목록: 프로젝트 전체의 모든 기호를 나열합니다.
  • 코드 작업: 주어진 텍스트 문서 및 범위에 대해 실행할 명령(일반적으로 정리/리팩터링)을 계산합니다.
  • 코드 렌즈: 주어진 텍스트 문서에 대한 코드 렌즈 통계를 계산합니다.
  • 문서 서식: 전체 문서, 문서 범위 서식 및 입력 시 서식이 포함됩니다.
  • 이름 바꾸기: 프로젝트 전체에서 기호의 이름을 바꿉니다.
  • 문서 링크: 문서 내에서 링크를 계산하고 해결합니다.
  • 문서 색상: 문서 내에서 색상을 계산하고 해결하여 편집기에서 색상 선택기를 제공합니다.

프로그래밍 가능한 언어 기능 주제는 위의 각 언어 기능을 설명하고 언어 서버 프로토콜을 통해 또는 확장에서 직접 확장 API를 사용하여 구현하는 방법에 대한 지침을 제공합니다.

점진적 텍스트 문서 동기화

이 예시에서는 vscode-languageserver 모듈에서 제공하는 간단한 텍스트 문서 관리자를 사용하여 VS Code와 언어 서버 간의 문서를 동기화합니다.

이것은 두 가지 단점이 있습니다.

  • 텍스트 문서의 전체 내용이 반복적으로 서버로 전송되므로 많은 데이터가 전송됩니다.
  • 기존 언어 라이브러리를 사용하는 경우, 이러한 라이브러리는 일반적으로 불필요한 구문 분석 및 추상 구문 트리 생성을 피하기 위해 점진적인 문서 업데이트를 지원합니다.

따라서 프로토콜은 점진적인 문서 동기화도 지원합니다.

점진적인 문서 동기화를 활용하려면 서버는 세 가지 알림 핸들러를 설치해야 합니다.

  • onDidOpenTextDocument: VS Code에서 텍스트 문서가 열릴 때 호출됩니다.
  • onDidChangeTextDocument: VS Code에서 텍스트 문서의 내용이 변경될 때 호출됩니다.
  • onDidCloseTextDocument: VS Code에서 텍스트 문서가 닫힐 때 호출됩니다.

아래는 이 알림 핸들러를 연결하는 방법과 초기화 시 올바른 기능을 반환하는 방법을 보여주는 코드 조각입니다.

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            // Enable incremental document sync
            textDocumentSync: TextDocumentSyncKind.Incremental,
            ...
        }
    };
});

connection.onDidOpenTextDocument((params) => {
    // A text document was opened in VS Code.
    // params.uri uniquely identifies the document. For documents stored on disk, this is a file URI.
    // params.text the initial full content of the document.
});

connection.onDidChangeTextDocument((params) => {
    // The content of a text document has change in VS Code.
    // params.uri uniquely identifies the document.
    // params.contentChanges describe the content changes to the document.
});

connection.onDidCloseTextDocument((params) => {
    // A text document was closed in VS Code.
    // params.uri uniquely identifies the document.
});

/*
Make the text document manager listen on the connection
for open, change and close text document events.

Comment out this line to allow `connection.onDidOpenTextDocument`,
`connection.onDidChangeTextDocument`, and `connection.onDidCloseTextDocument` to handle the events
*/
// documents.listen(connection);

VS Code API를 직접 사용하여 언어 기능 구현

언어 서버는 많은 이점을 가지고 있지만, VS Code의 편집 기능을 확장하는 유일한 옵션은 아닙니다. 특정 유형의 문서에 간단한 언어 기능을 추가하려는 경우 vscode.languages.register[LANGUAGE_FEATURE]Provider를 옵션으로 고려하십시오.

다음은 일반 텍스트 파일에 대한 자동 완성으로 몇 가지 스니펫을 추가하기 위해 vscode.languages.registerCompletionItemProvider를 사용하는 completions-sample입니다.

VS Code API 사용법을 보여주는 더 많은 샘플은 https://github.com/microsoft/vscode-extension-samples 에서 찾을 수 있습니다.

언어 서버용 오류 허용 파서

대부분의 경우 편집기의 코드는 불완전하고 구문적으로 올바르지 않지만, 개발자는 여전히 자동 완성 및 기타 언어 기능이 작동할 것으로 기대합니다. 따라서 오류 허용 파서는 언어 서버에 필수적입니다. 파서는 부분적으로 완료된 코드에서 의미 있는 AST를 생성하고, 언어 서버는 AST를 기반으로 언어 기능을 제공합니다.

VS Code에서 PHP 지원을 개선할 때 공식 PHP 파서는 오류 허용이 아니며 언어 서버에서 직접 재사용할 수 없다는 것을 알게 되었습니다. 따라서 Microsoft/tolerant-php-parser를 개발했으며, 오류 허용 파서 구현이 필요한 언어 서버 작성자에게 도움이 될 수 있는 자세한 메모를 남겼습니다.

자주 묻는 질문

서버에 연결하려고 할 때 "런타임 프로세스에 연결할 수 없습니다(5000ms 후 시간 초과)" 오류가 발생합니다.

디버거를 연결하려고 할 때 서버가 실행되고 있지 않으면 이 시간 초과 오류가 발생합니다. 클라이언트가 언어 서버를 시작하므로 실행 중인 서버가 있는지 확인하려면 클라이언트를 시작해야 합니다. 또한 클라이언트 중단점을 비활성화해야 할 수도 있습니다. 이 중단점이 서버 시작을 방해하는 경우입니다.

이 가이드와 LSP 사양을 읽었지만 여전히 해결되지 않은 질문이 있습니다. 어디에서 도움을 받을 수 있나요?

https://github.com/microsoft/language-server-protocol 에서 이슈를 열어주세요.

© . This site is unofficial and not affiliated with Microsoft.