개발 기술/개발 이야기

[vscode] 컬러 변수 뷰어 만들기(2) - colorvariabletracker

by GicoMomg 2024. 3. 31.

1. colorvariabletracker

  • 이번 포스팅은 “[vscode] 컬러 변수 뷰어 만들기” 시리즈의 마지막인 “extension 구현하기”이다.
  • 구현한 extension은 colorvariabletracker 로, 현재 확장을 릴리즈한 상태이다. (링크)
  • 컬러 변수 뷰어(colorvariabletracker)는 특정 파일에 선언된 컬러 변수를 추적하는 확장이다.
  • 컬러 변수 뷰어에서 구현한 기능은 6가지이다.
  • 이번 시간에는 이 6가지를 구현한 방법은 간단하게 설명하고자 한다 🙂
기능 방식
추적하고 싶은 파일을 설정할 수 있다. .vscode/settings.json에 파일 경로 지정
선언된 컬러변수를 vscode 사이드바에서 표 형식으로 볼 수 있어야 한다. webview로 구현
검색창에서 컬러 변수명 혹은 컬러값 입력시 해당하는 컬러변수를 볼 수 있다. 검색어에 해당하는 컬러변수를 webview에 보여줌
현재 추적하고 컬러 파일을 열 수 있다. vscode의 showTextDocument() 사용
현재 추적하고 있는 컬러값이 수정되면, 사이드바에 반영된다. 파일 변경을 감지하여, webview 업데이트
사이드바에서 클립보드로 컬러를 복사할 수 있다. vscode의 clipboard.writeText() 사용

 

잠깐! 이 포스팅은 webview에 대한 지식이 필요하므로, 만약 webview를 모른다면 이전 포스팅을 읽는 건 추천한다. (이전 포스팅은 여기!)



2. 구현 방식 살펴보기

1) 추적할 파일 경로 설정하기

(1) 컨셉

  • 확장을 사용하기 위해, 먼저 추적할 컬러 파일의 상대 경로를 지정해야 한다.
  • .vscode/settings.json 에 추적하고 싶은 컬러 파일의 상대 경로를 추가하도록 설정했다.
  • 예를 들어 경로를 “/color.scss”로 설정하면, 루트위치의 color.scss를 추적한다는 의미가 된다.
  • 내부 구현에서는 어떻게 이 데이터에 접근해 파일 경로를 설정했을까?
// .vscode/settings.json

{
  "colorVariableTracker.filePath": "/color.scss"
}

 

(2) 구현 방식

  • 방식은 확장이 구동될 때, vscode.workspace.getConfiguration()을 호출해 파일 경로를 구성하는 것이다.
  • resolveWebviewView은 초기 webview를 설정하는 함수인데, 이 함수 내부에 파일을 탐색하는 코드를 추가한다.
 // SidebarProvider.ts

 public async resolveWebviewView(webviewView: vscode.WebviewView) {
     registerColorFilePath(); // 파일 경로 추적 함수
 };

 

  • registerColorFilePath()은 확장이 구동될 때, 추적할 파일 경로를 구성한다.
// SidebarProvider.ts

public registerColorFilePath() {
  new Promise((resolve, reject) => {
    // (a)
    const config = vscode.workspace.getConfiguration('colorVariableTracker'); 
    const filePath = config.get('filePath') as string; 

    // (b)
    if (!filePath) {
      vscode.window.showErrorMessage('Please set a color file path in /.vscode/settings.json');
    } 
    // (c)
    else if (!filePath.includes('.css') && !filePath.includes('.scss') && !filePath.includes('.sass')) {
      vscode.window.showErrorMessage('Color file must be a .css or .scss or .sass file.');
    } 
    // (d)
    else {
      this._colorFilePath = `${this._baseFilePath}${filePath}`;
      vscode.window.showInformationMessage(`Color file path is set to ${this._colorFilePath}`);
    } 
    resolve('');
  });
}
코드번호 설명
(a) - vscode.workspace.getConfiguration을 사용해 .vscode/settings.json에 접근한다.
- 그리고 colorVariableTracker섹션에서 filePath를 가져온다.
(b) - filePath가 없다면, 경고 메시지를 표시한다.
(c) - 가져온 파일 경로가 올바르지 않으면(확장자가 .css, .scss, 또는 .sass가 아니면), 경고 메시지를 표시한다.
(d) - 파일 경로가 올바르다면, 내부 변수 _colorFilePath에 파일 경로를 저장한다.
- 그리고 사용자에게 파일 경로가 설정되었다는 정보 메시지를 표시한다.



2) webview로 컬러변수 보여주기

(1) 컨셉

  • 사이드바에 컬러변수명, 변수값을 보여주기 위해 표 형식으로 데이터를 렌더링해야한다.
  • webview는 html 형식으로 데이터를 표현할 수 있는데, getHtmlForWebview()에 원하는 데이터를 넣어서 html 형식을 렌더링하면 된다.
  • 아래 코드는 webviewhtml를 렌더링하는 간단 예시이다.
 // 초기 webview 렌더링하는 함수
 public async resolveWebviewView(webviewView: vscode.WebviewView) {
    ...   
    webviewView.webview.html = this.getHtmlForWebview(webviewView.webview);
    this._view = webviewView;
  }
 private getHtmlForWebview(webview: vscode.Webview) {
    return `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
      </head>
      <body>
            <!-- 여기에 원하는 html 태그 추가 -->
      </body>
    </html>`;
  }
};

 

(2) 구현 방식

  • 그렇다면, 표 형식으로 데이터를 띄우려면 어떻게 구현해야할까?
  • 구현 방식은 (a) 추적하는 파일의 데이터를 탐색하여 (b) 선언된 변수 중 값이 컬러인 데이터를 찾아서 (c) html형태로 렌더링하면 된다.
 // SidebarProvider.ts

 // 초기 webview 렌더링하는 함수
 public async resolveWebviewView(webviewView: vscode.WebviewView) {
        // (a)
    await this.registerColorFilePath();

    // (b)
    this._docText = await this.getColorVariables();

    // (c)
    webviewView.webview.options = { enableScripts: true, localResourceRoots: [this._extensionUri] };
    webviewView.webview.html = this.getHtmlForWebview(webviewView.webview);

    this._view = webviewView;
  }
코드번호 설명
(a) - 추적할 컬러 파일의 상대 경로를 지정한다. 상대 경로는 _colorFilePath 변수에 저장된다.
(b) - _colorFilePath 경로에 있는 파일을 탐색하여, 컬러변수값을 _docText에 저장한다.
- this._docTextgetHtmlForWebview()에서 쓰인다.
(c) - webviewView.webview.htmlgetHtmlForWebview()값을 넣어서, html를 렌더링한다.

 

  • getColorVariables()openTextDocument()로 파일 데이터에 접근하여, html형식으로 포맷팅된 값을 리턴한다.
// SidebarProvider.ts

private async getColorVariables() {
  if (!this._colorFilePath) return '';
  try {
    const document = await vscode.workspace.openTextDocument(this._colorFilePath);
    return this.formatColorVariables(document.getText());
  } catch (error) {
    return 'No color variables found in the file.';
  }
}

 

  • formatColorVariables()는 표 형식으로 html를 구성하는 함수이다.
// SidebarProvider.ts

private formatColorVariables(text: string) {
  const colorObj = getColorVariableObj(text, this._searchText);
  const colorText: string[] = [];

  Object.keys(colorObj).forEach((key) => {
    const row = `
      <tr>
        <td>${key}</td>
        <td>
          <button class="colorBox" 
            style="background-color:${colorObj[key]}" 
            title="${colorObj[key]}" 
            data-color="${colorObj[key]}"
            >
          </button>
        </td>
      </tr>`;
    colorText.push(row);
    }
  );

  return colorText.join("");
};

 

  • getColorVariableObj()는 정규 표현식으로, 선언된 변수 중 컬러값을 가지는 변수만을 리턴해준다.
// SidebarProvider.ts

function getColorVariableObj(cssCode: string) {
  const pattern = /(--[\w-]+|\$[\w-]+)\s*:\s*(#[0-9a-fA-F]{3,6}|rgba?\([^)]*\));/g;
  const matches = cssCode.matchAll(pattern);
  const filteredMatches = Array.from(matches).filter((match) => {
    return match[1].includes(searchText) || match[2].includes(searchText);
  });

  const colorVariable: { [key: string]: string } = {};
  for (const match of filteredMatches) {
    colorVariable[match[1]] = match[2];
  }
  return colorVariable;
}

 

  • 앞선 과정을 거쳐 리턴된 html 코드는 _docText에 저장되는데,
  • getHtmlForWebview()${_docText}를 삽입하여 렌더링하면 완성이다!
// SidebarProvider.ts

private getHtmlForWebview(webview: vscode.Webview) {
    const styleResetUri = webview.asWebviewUri(
      vscode.Uri.joinPath(this._extensionUri, "media", "reset.css")
    );
    const styleVSCodeUri = webview.asWebviewUri(
      vscode.Uri.joinPath(this._extensionUri, "media", "vscode.css")
    );

    return `<!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel='stylesheet' href='${styleResetUri}' />
        <link rel='stylesheet' href='${styleVSCodeUri}' />
      </head>
      <body>
      <table>
        <thead>
            <tr>
              <th>Variable Name</th>
              <th>Color Value</th>
            </tr>
        </thead>
        <tbody>
          ${ this._docText }  <!-- 변수 테이블 삽입 -->
        </tbody>
      </table>
      </body>
    </html>`;

  }
}

 

  • 실제 렌더링 예시는 아래와 같다.



3) 검색어에 해당하는 컬러변수를 webview에 보여주기

(1) 컨셉

  • 검색창에 컬러 변수명이나 변수값을 입력하고, 검색을 눌렀을 때 결과값을 보여주고자 한다.
  • 그렇다면, 검색창, 버튼 그리고 이벤트는 어떻게 등록할 수 있을까?
  • webview는 html 형식을 렌더링할 수 있기에, 버튼이나 입력창도 추가할 수 있다.
  • 또한 <body><script>태그를 사용해 이벤트도 선언할 수 있다.
  • 그래서 검색창이나 검색 버튼을 추가하고, 버튼 클릭시 그에 따른 데이터 조작이 가능하다.
  • 만약 버튼이 하나 있다고 가정했을 때, 이벤트 동작 흐름은 아래와 같다.
선언 위치 동작
웹뷰 > body > button - 사용자가 웹뷰에서 버튼을 클릭한다.
웹뷰 > script에 선언된 이벤트 - 클릭 이벤트가 감지되면, 이벤트 리스너에 의해 등록된 함수가 호출된다.
  - 호출된 함수에서 acquireVsCodeApi() 를 사용하여 VSCode API를 가져온다.
- 이를 통해 VSCode 확장 프로그램과 웹뷰 간의 통신이 가능하다.
  - acquireVsCodeApi().postMessage로 VSCode 확장 프로그램으로 메시지를 보낸다.
vscode - VSCode 확장 프로그램은 이 메시지를 수신하여 특정 동작을 실행한다.

 

(2) 구현 방식

  • 검색 관련 기능은 (1) 검색어 업데이트, (2) Enter로 검색하기, (3) 검색하기 버튼, (4) 검색어 초기화이다.
  • 이 중 (1) 검색어 업데이트, (2) Enter로 검색하기 기능만 살펴보자.
  • 우선 검색폼과 검색하기 버튼을 HTML 렌더링 함수에 추가한다.
// SidebarProvider.ts

private getHtmlForWebview(webview: vscode.Webview) {
    return `
    <!DOCTYPE html>
    <html lang="en">
      ...
      <body>
      <div class="search-bar">
        <div class="input-wrapper">
          <input 
              type="text" 
              id="searchInput" 
              value="${this._searchText}" 
              placeholder="search color..."
          >
        </div>
        <button type="button" id="btnSearch">Search</button>
      </div>
      <table> ... </table>
      </body>
    </html>`;
    ..  
  }
}



  • <script>에 입력, 검색 버튼 이벤트를 등록해야 한다.
  • 동작은 검색어 입력시 → vscode에 선언한 _searchText 변수를 업데이트하고,
  • 검색 버튼 클릭시 → _searchText로 컬러변수표를 필터링하여 webview를 업데이트해야 한다.
  • vscode에 선언한 변수와 webview를 컨트롤하기 위해, vscode에 메시지를 송신하는 방식을 써야 한다.
  • 이 기능을 위해 총 2개의 메시지를 선언해서 사용했다.
vscode 메시지타입 이벤트 수신시 동작
onInput - _searchText를 업데이트한다.
- _searchText값에 따라 컬러변수표를 필터링한다.
onUpdate - webview를 업데이트한다.

 

  • 먼저 메시지를 사용하기 위해, 초기 webview 렌더링시 onDidReceiveMessage()로 메시지를 등록한다.
// SidebarProvider.ts

// 초기 webview 렌더링 함수
public async resolveWebviewView(webviewView: vscode.WebviewView) {
  ...
  // 추가!
  webviewView.webview.onDidReceiveMessage(this.onReceiveMessage.bind(this)); 
  this._view = webviewView;
}

 

  • onReceiveMessage()은 송신한 메시지 타입에 따라, 특정 함수를 실행한다.
// SidebarProvider.ts

private async onReceiveMessage(data: { type: string; value?: any }) {
  switch (data.type) {
    case "onInput": {
      this._searchText = data.value; // (a)
      break;
    }

    case "onUpdate": {
      this.update(this._doc); // (b)
      break;
    }
    ...
}

public async update(doc?: vscode.TextDocument) {
  else if (doc.uri.path !== this._colorFilePath) return;
  ...
  this._view.webview.html = this.getHtmlForWebview(this._view.webview);
}
코드번호 설명
(a) - onInput은 입력폼에서 검색어 입력시 검색어 변수(_searchText)를 업데이트하는 이벤트이다.
- 유저가 입력폼에 텍스트를 입력하면 onInput 이벤트를 호출해 _searchText를 입력값으로 변경한다.
(b) - onUpdate는 검색하기, 검색어 초기화하기 실행시 webview를 업데이트하는 이벤트이다.
- 검색하기, 검색어 초기화 버튼 클릭시, webview를 업데이트한다.
- 이때 webview에서는 _searchText에 해당하는 데이터만 보여주도록 컬러변수표를 재구성한다.

 

  • 마지막으로 <script>에 각 요소별 이벤트 핸들러를 추가한다.
// SidebarProvider.ts

private getHtmlForWebview(webview: vscode.Webview) {
    return `
      ...
      <script>
        (function() {
            // (a)
            const vscode = acquireVsCodeApi();

            // (b)
            const btnSearch = document.getElementById('btnSearch');
            const searchInput = document.getElementById('searchInput');

            // (c)
            btnSearch.addEventListener('click', () => {
              vscode.postMessage({ type: 'onInput', value: searchInput.value });
              vscode.postMessage({ type: 'onUpdate' });
            });

            // (d)
            searchInput.addEventListener('keyup', (e) => {
              vscode.postMessage({ type: 'onInput', value: searchInput.value });
              if (e.key === 'Enter') vscode.postMessage({ type: 'onUpdate' });
            });
        }())
    </script>
      </body>
    </html>`;

  }
}
코드 번호 설명
(a) - acquireVsCodeApi() 함수를 사용하여 vscode 객체를 가져온다.
- 이 객체를 사용하여 VSCode 확장 프로그램과 웹뷰 간의 통신을 할 수 있다.
(b) - HTML 요소들을 가져온다.
- btnSearch는 검색 버튼을, searchInput은 검색 입력란이다.
(c) 검색 버튼시, vscode.postMessage로 VSCode에 메시지를 전송한다.
- onInput 메시지에는 현재 검색한 단어를 전송하여, 외부 변수를 업데이트하고, onUpdate 메시지를 송신해 webview를 업데이트한다.
(d) - 검색어 입력시, vscode.postMessage로 VSCode에 메시지를 전송한다.
- onInput 메시지에는 현재 검색한 단어를 전송하여, 외부 변수를 업데이트하고,
만약 Enter키 입력시 onUpdate 메시지를 송신해 webview를 업데이트한다.

 

  • 그러면 검색어를 입력하여 컬러변수표를 업데이트할 수 있다.



4) 변경을 감지하여, webview 업데이트하기

(1) 구현 방식

  • vscode 이벤트를 사용하면 특정 파일이 업데이트 되었을 때 webview를 업데이트할 수 있다!
  • onDidSaveTextDocument 이벤트는 사용자가 문서를 저장할 때마다 발생한다.
  • onDidSaveTextDocument 이벤트가 발생할 때마다 update() 함수를 실행하자
// extension.ts

import * as vscode from 'vscode';
import { SidebarProvider } from './SidebarProvider';

export function activate(context: vscode.ExtensionContext) {
    const sidebarProvider = new SidebarProvider(context.extensionUri);
   ...

  // 추가!
    context.subscriptions.push(vscode.workspace.onDidSaveTextDocument((e) => {
        sidebarProvider.update(e);
    }));

}

 

  • update()는 현재 수정된 파일과 추적 중인 파일이 같으면, webview를 업데이트한다.
// SidebarProvider.ts

public async update(doc?: vscode.TextDocument) {
  else if (doc.uri.path !== this._colorFilePath) return;
  ...
  this._view.webview.html = this.getHtmlForWebview(this._view.webview);
}

 

  • 이를 통해, 현재 추적 중인 파일이 수정될 때마다 데이터가 동적으로 변경된다.



5) 현재 추적하고 컬러 파일 열기

(1) 컨셉

  • 만약 vscode에서 특정 파일을 열고 싶다면, showTextDocument()를 사용하면 된다.
  • showTextDocument()에 열고 싶은 파일의 경로를 넘기고 호출하면, 대상 파일을 열 수 있다.
vscode.window.showTextDocument(path);

 

(2) 구현 방식

  • 확장에서 [Open Color File] 클릭시, 현재 추적하고 있는 컬러 파일을 열도록 했다.
  • 동작은 버튼 클릭시 postMessage()로 이벤트를 전송해서 파일을 여는 방식이다.
  • 이 동작을 위해, 타입이 “openFile”인 메시지를 등록한다.
  • 이 메시지가 수신되면, showTextDocument()로 현재 추적 중인 파일(this._colorFilePath)을 연다.
// SidebarProvider.ts

private async onReceiveMessage(data: { type: string; value?: any }) {
  switch (data.type) {
    case "openFile": {
      if (!this._colorFilePath) {
        vscode.window.showErrorMessage("Color file not found");
        return;
      }
      vscode.window.showTextDocument(vscode.Uri.file(this._colorFilePath));
      break;
    }
    ...
  }
}

 

  • webview에 버튼을 추가하고, <script>에서 버튼 클릭시 “openFile” 이벤트를 송신한다.
// SidebarProvider.ts

private getHtmlForWebview(webview: vscode.Webview) {
    return `
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
      </head>
      <body>
      <div class="search-bar"> ... </div>
      <table>
           <button id="btnOpen">Open Color File</button> 
        ...
      </table>
      <script>
        (function() {
            const vscode = acquireVsCodeApi();
            const btnOpen = document.getElementById('btnOpen');

            btnOpen.addEventListener('click', () => {
              vscode.postMessage({ type: 'openFile' });
            });
            ...
        }())
    </script>
      </body>
    </html>`;
  }
}

 

  • 그러면 아래와 같이 동작이 진행된다.



6) 클립보드 형식으로 컬러 복사하기

  • vscode에서 클립보드를 하고 싶다면? vscode.env.clipboard.writeText()를 사용하자!
  • writeText()에 데이터를 인자로 넘기면, 해당 데이터가 복사된다.
vscode.env.clipboard.writeText('내가 복사될 거야!');

 

  • 구현 방식의 경우, 이전과 동일하게 postMessage를 활용하면 된다.
 // SidebarProvider.ts

 private getHtmlForWebview(webview: vscode.Webview) {
    // html의 script 부분
    return `
        <script>
        ...
            const vscode = acquireVsCodeApi();
            const colorBoxes = document.querySelectorAll('.colorBox');

            colorBoxes.forEach((box) => {
              box.addEventListener('click', (e) => {
                const color = e.target.dataset.color;
                vscode.postMessage({ type: 'copyToClipboard', value: color });
              });
            });
        </script>
    `;
}
// SidebarProvider.ts

private async onReceiveMessage(data: { type: string; value?: any }) {
  switch (data.type) {
    case "copyToClipboard": {
      if (!data.value) return;

      vscode.env.clipboard.writeText(data.value);
      vscode.window.showInformationMessage(`Copied ${data.value} to clipboard`);
      break;
    }
  };

 

  • 구현된 동작은 아래와 같다.



2. 마치며…

  • 이번 시간에는 컬러 변수 뷰어 만들기 시리즈의 마지막인, extension 만드는 법을 알아보았다.
  • 이 포스팅에서는 postMessage로 메시지를 송수신하여 함수를 호출하는 법, clipboard.writeText로 클립보드하는 법,
    webview로 html을 주입하는 법 등 여러가지 방법을 알 수 있었다.
  • 포스팅에 사용한 코드는 colorvariabletracker 확장의 일부인데, 만약 전체 코드를 보고 싶다면 여기, 확장 기능을 구경하고 싶다면 여기를 참고하면 된다.



반응형

댓글