Skip to content

Latest commit

 

History

History
189 lines (152 loc) · 8.22 KB

03-language-server.mdx

File metadata and controls

189 lines (152 loc) · 8.22 KB
title description date tags
03. Language server
nn 개발기 3편
2024-10-03
blog

이번 글은 언어 서버에 대해 설명하려 한다.

다만 예전 구현은 별로 설명할만한 구석이 없어서 현재(2024-10-03) 기준으로 글을 작성하였다.

또한, 구현 중 참고했던 Typescript Language Server (이하 TSServer)코드도 같이 설명했으니 알아두길 바란다.

2024-10-03 aa93904

큰 틀에서의 구조는 TSServer와 크게 다르지 않은데,

export function createLspConnection(options: LspConnectionOptions) {
  const connection = createConnection(ProposedFeatures.all);
  const client = new LspClient(connection);
  const logger = new LspClientLogger(client, options.showMessageLevel);
  const documents = new TextDocuments(TextDocument);

  const context: Partial<LspContext> = {
    logger,
    client,
    documents,
    showMessageLevel: options.showMessageLevel
  }

  connection.onDidOpenTextDocument((params) => openTextDocument(params, context as LspContext))
  connection.onDidCloseTextDocument((params) => onDidCloseTextDocument(params, context as LspContext))
  documents.onDidChangeContent((params) => onDidChangeTextDocument(params, context as LspContext))

  connection.onInitialize((params) => initialize(params, context))
  connection.onCompletion((params, token) => completion(params, context as LspContext, token))
  connection.onHover((params, token) => hover(params, context as LspContext, token))

  connection.languages.semanticTokens.on((params) => semanticTokens(params, context as LspContext))

  documents.listen(connection);
  return connection;
}
export function createLspConnection(options: LspConnectionOptions): lsp.Connection {
    const connection = lsp.createConnection(lsp.ProposedFeatures.all);
    const lspClient = new LspClientImpl(connection);
    const logger = new LspClientLogger(lspClient, options.showMessageLevel);
    const server: LspServer = new LspServer({
        logger,
        lspClient,
    });

    connection.onInitialize(server.initialize.bind(server));
    connection.onInitialized(server.initialized.bind(server));
    connection.onDidChangeConfiguration(server.didChangeConfiguration.bind(server));

    connection.onDidOpenTextDocument(server.didOpenTextDocument.bind(server));
    connection.onDidSaveTextDocument(server.didSaveTextDocument.bind(server));
    connection.onDidCloseTextDocument(server.didCloseTextDocument.bind(server));
    connection.onDidChangeTextDocument(server.didChangeTextDocument.bind(server));

    connection.onCodeAction(server.codeAction.bind(server));
    connection.onCodeLens(server.codeLens.bind(server));
    connection.onCodeLensResolve(server.codeLensResolve.bind(server));
    connection.onCompletion(server.completion.bind(server));
    connection.onCompletionResolve(server.completionResolve.bind(server));
    connection.onDefinition(server.definition.bind(server));
    connection.onImplementation(server.implementation.bind(server));
    connection.onTypeDefinition(server.typeDefinition.bind(server));
    connection.onDocumentFormatting(server.documentFormatting.bind(server));
    connection.onDocumentRangeFormatting(server.documentRangeFormatting.bind(server));
    connection.onDocumentHighlight(server.documentHighlight.bind(server));
    connection.onDocumentSymbol(server.documentSymbol.bind(server));
    connection.onExecuteCommand(server.executeCommand.bind(server));
    connection.onHover(server.hover.bind(server));
    connection.onReferences(server.references.bind(server));
    connection.onRenameRequest(server.rename.bind(server));
    connection.onPrepareRename(server.prepareRename.bind(server));
    connection.onSelectionRanges(server.selectionRanges.bind(server));
    connection.onSignatureHelp(server.signatureHelp.bind(server));
    connection.onWorkspaceSymbol(server.workspaceSymbol.bind(server));
    connection.onFoldingRanges(server.foldingRanges.bind(server));
    connection.languages.onLinkedEditingRange(server.linkedEditingRange.bind(server));
    connection.languages.callHierarchy.onPrepare(server.prepareCallHierarchy.bind(server));
    connection.languages.callHierarchy.onIncomingCalls(server.callHierarchyIncomingCalls.bind(server));
    connection.languages.callHierarchy.onOutgoingCalls(server.callHierarchyOutgoingCalls.bind(server));
    connection.languages.inlayHint.on(server.inlayHints.bind(server));
    connection.languages.semanticTokens.on(server.semanticTokensFull.bind(server));
    connection.languages.semanticTokens.onRange(server.semanticTokensRange.bind(server));
    connection.workspace.onWillRenameFiles(server.willRenameFiles.bind(server));

    return connection;
}

위쪽 코드가 nn의 lsp 메소드별 라우팅, 아래쪽 코드가 TSServer가 구현하고 있는 lsp 메소드별 라우팅이다.

다만 라우팅 하위 코드를 TSServer에서는 class로 구현하고 있는 반면,

export async function hover(params: TextDocumentPositionParams, context: LspContext, token?: CancellationToken): Promise<Hover | null> {

nn 구현에서는 context 객체를 따로 만들어서 인자로 넣어주고 있다. (큰 이유는 없지만 class 문법의 장황함과 indent가 맘에 안들었다.)

그 외 설명할만한 포인트가 하나 있는데,

private triggerDiagnostics(delay: number = 200): void {
    this.diagnosticDelayer.trigger(() => {
        this.sendPendingDiagnostics();
    }, delay);
}

diagnostic을 전달하기 전에 일부러 200ms의 딜레이를 준다.

이유는 사용자의 키 스트로크가 진행되는 중에는 diagnostic이 실시간으로 변하지 않게 하기 위함이다.

(이 기능 때문에 TSServer에서 diagnostic을 전달하는 코드가 엄청 복잡해졌는데, 간단히 설명하자면 이미 발생한 요청을 취소하기 위해 최소 2~3개의 클래스를 더 만들어야 했다.)

해당 기능도 구현하기로 마음을 먹어

export class Delayer<T> {
  constructor(  
    public defaultDelay: number,
    private timeout: NodeJS.Timeout | null = null,
    private completionPromise: Promise<T> | null = null,
    private onSuccess: ((value: T) => void) | null = null,
    private task: (() => T) | null = null,
  )
  {
  }

  public trigger(task: () => T, delay: number = this.defaultDelay): Promise<T> {
    this.task = task;
    
    if (delay >= 0) {
      this.cancelTimeout();
    }

    if (!this.completionPromise) {
      this.completionPromise = new Promise<T>((resolve) => {
        this.onSuccess = resolve;
      }).then(() => {
        this.completionPromise = null;
        const result = this.task!();
        this.task = null;
        return result;
      });
    }

    if (delay >= 0 || this.timeout === null) {
      this.timeout = setTimeout(() => {
        this.timeout = null;
        this.onSuccess!(null!);
      }, delay >= 0 ? delay : this.defaultDelay);
    }

    return this.completionPromise;
  }

  public cancelTimeout(): void {
    if (this.timeout !== null) {
      clearTimeout(this.timeout);
      this.timeout = null;
    }
  }
}

delayer 코드를 구현하고 적용했다.


아무래도 아직 구현할 게 많기 때문에, 나중에 관련해서 글을 또 써야할 것 같다.

다음 주제는 (아마도) 타입 체커 세부 구현일 것 같다.