Writing in the front

With A wave of bull market in July, more and more people join the A-share market, but the risk of the stock market is huge, some people become rich overnight, some people lose everything, so for ordinary people, the fund is A good choice, I am also A small leek fund.

When I go to work, I am often mentally itching to see how much money THE fund has earned (GE) today. The procedure of taking out my mobile phone and opening Alipay is too complicated. Besides, I don’t care much about other indicators, but I just want to know today’s net value and increase. VS Code as a coding tool provides a powerful plugin mechanism that we can take advantage of to see what’s going on as we Code. You can install the plugin by searching for “fund-watch” in VS Code.

Implement the plugin

Initialize the

VSCode has a very handy plugin template that you can generate directly from Yeoman.

Install yo and generator-code globally and run the yo code command.

Install the YO module globally
npm install -g yo generator-code
Copy the code

Here we use TypeScript to write plug-ins.

The generated directory structure is as follows:

The VS Code plug-in can be understood simply as an Npm package, which also requires a package.json file with the same properties as the Npm package.

{
  / / name
  "name": "fund-watch"./ / version
  "version": "1.0.0"./ / description
  "description": "Check the fund in real time."./ / publisher
  "publisher": "shenfq".// Version requirements
  "engines": {
    "vscode": "^ 1.45.0"
  },
  // Import file
  "main": "./out/extension.js"."scripts": {
    "compile": "tsc -p ./"."watch": "tsc -watch -p ./",},"devDependencies": {
    "@types/node": "^ 10.14.17"."@types/vscode": "^ 1.41.0"."typescript": "^ 3.9.7." "
  },
  // Plug-in configuration
  "contributes": {},
  // Activate events
  "activationEvents": [],}Copy the code

This section describes some important configurations.

  • contributes: Configures plug-ins.
  • activationEvents: Activates the event.
  • main: entry file for the plug-in, which behaves like the Npm package.
  • namepublisher: name is the name of the plug-in, publisher is the publisher.${publisher}.${name}Constitutes the plug-in ID.

Of particular interest are the contributes and activationEvents configurations.

Create a view

We first create a view in our container, view container is simply a separate sidebar, in a package. The json contributes. ViewsContainers configured.

{
  "contributes": {
    "viewsContainers": {
      "activitybar": [{"id": "fund-watch"."title": "FUND WATCH"."icon": "images/fund.svg"}]}}}Copy the code

Json contributes. This field is an object whose Key is the ID of our view container. The value is an array, indicating that multiple views can be added to a view container.

{
  "contributes": {
    "viewsContainers": {
      "activitybar": [{"id": "fund-watch"."title": "FUND WATCH"."icon": "images/fund.svg"}},"views": {
      "fund-watch": [{"name": "Optional Fund"."id": "fund-list"}]}}}Copy the code

If you don’t want to add them to a custom view container, you can choose VS Code’s own view container.

  • explorer: is displayed in the Explorer sidebar
  • debug: displays in the debug sidebar
  • scm: is displayed in the source sidebar
{
  "contributes": {
    "views": {
      "explorer": [{"name": "Optional Fund"."id": "fund-list"}]}}}Copy the code

Run the plugin

Templates generated using Yeoman come with VS Code runtime capabilities.

Switch to the debug panel, click Run, and you’ll see an icon in the sidebar.

Add the configuration

We need to get a list of funds, and of course some fund codes, which we can put into VS Code’s configuration.

{
  "contributes": {
    / / configuration
    "configuration": {
      // Configure type, object
      "type": "object".// Configure the name
      "title": "fund".// Configure the attributes
      "properties": {
        // Select a list of funds
        "fund.favorites": {
          // Attribute type
          "type": "array"./ / the default value
          "default": [
            "163407"."161017"]./ / description
          "description": "List of optional Funds, value is fund code"
        },
        // Refresh interval
        "fund.interval": {
          "type": "number"."default": 2."description": "Refresh time, in seconds. Default is 2 seconds."
        }
      }
    }
  }
}
Copy the code

View data

Let’s go back to the previously registered view, which is called a tree view in VS Code.

"views": {
  "fund-watch": [{"name": "Optional Fund"."id": "fund-list"}}]Copy the code

We need to provide data to the view through the registerTreeDataProvider provided by vscode. Open the generated SRC /extension.ts file and modify the code as follows:

// The vscode module is built-in to VS Code and does not need to be installed via NPM
import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// Activate the plugin
export function activate(context: ExtensionContext) {
  / / class
  const provider = new Provider();

  // Data registration
  window.registerTreeDataProvider('fund-list', provider);
}

export function deactivate() {}
Copy the code

Here we through VS Code provided by the window. The registerTreeDataProvider registration data, the first parameter to the incoming said view ID, the second parameter is the TreeDataProvider implementation.

The TreeDataProvider has two methods that must be implemented:

  • getChildrenThis method takes an element and returns a child of the element. If there is no element, it returns a child of the root node.
  • getTreeItemThis method accepts an Element and returns the view’s single row of UI data, required for theTreeItemInstantiate;

Let’s use VS Code’s Explorer to show these two methods:

With this knowledge, we can easily provide data for the tree view.

import { workspace, TreeDataProvider, TreeItem } from 'vscode';

export default class DataProvider implements TreeDataProvider<string> {
  refresh() {
    // Update the view
  }

  getTreeItem(element: string): TreeItem {
    return new TreeItem(element);
  }

  getChildren(): string[] {
    const { order } = this;
    // Get the configured fund code
    const favorites: string[] = workspace
      .getConfiguration()
      .get('fund-watch.favorites'[]);// Sort by code
		return favorites.sort((prev, next) = > (prev >= next ? 1 : -1) * order); }}Copy the code

When you run it now, you might find that there is no data on the view because activation events are not configured.

{
	"activationEvents": [
    // Activate the plugin when the fund-list view is displayed
		"onView:fund-list"]}Copy the code

The request data

Now that we have successfully displayed the fund code on the view, we need to request the fund data. There are many fund related apis on the Internet. Here we use data from Tiantian Fund Network.

As can be seen from the request, Tiantian Fund network obtains fund related data through JSONP. We only need to construct a URL and pass in the current timestamp.

const url = `https://fundgz.1234567.com.cn/js/${code}.js? rt=${time}`
Copy the code

VS Code request data, need to use internal HTTPS module, let’s create a new api.ts.

import * as https from 'https';

// Initiate a GET request
const request = async (url: string) :Promise<string> = > {return new Promise((resolve, reject) = > {
    https.get(url, (res) = > {
      let chunks = ' ';
      if(! res || res.statusCode ! = =200) {
        reject(new Error('Network request error! '));
        return;
      }
      res.on('data'.(chunk) = > chunks += chunk.toString('utf8'));
      res.on('end'.() = > resolve(chunks));
    });
  });
};

interface FundInfo {
  now: string
  name: string
  code: string
  lastClose: string
  changeRate: string
  changeAmount: string
}

// Request fund data according to the fund code
export default function fundApi(codes: string[]) :Promise<FundInfo[] >{
  const time = Date.now();
	// Request list
  const promises: Promise<string>[] = codes.map((code) = > {
    const url = `https://fundgz.1234567.com.cn/js/${code}.js? rt=${time}`;
    return request(url);
  });
  return Promise.all(promises).then((results) = > {
    const resultArr: FundInfo[] = [];
    results.forEach((rsp: string) = > {
      const match = rsp.match(/jsonpgz\((.+)\)/);
      if(! match || ! match[1]) {
        return;
      }
      const str = match[1];
      const obj = JSON.parse(str);
      const info: FundInfo = {
        // Current net worth
        now: obj.gsz,
        // Fund name
        name: obj.name,
        // Fund code
        code: obj.fundcode,
        // Yesterday's net worth
        lastClose: obj.dwjz,
        / / price
        changeRate: obj.gszzl,
        / / or forehead
        changeAmount: (obj.gsz - obj.dwjz).toFixed(4),}; resultArr.push(info); });return resultArr;
  });
}
Copy the code

Next, modify the view data.

import { workspace, TreeDataProvider, TreeItem } from 'vscode';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // Other code omitted
  getTreeItem(info: FundInfo): TreeItem {
    // Display the name and drop
  	const { name, changeRate } = info
    return new TreeItem(`${name}  ${changeRate}`);
  }

  getChildren(): Promise<FundInfo[]> {
    const { order } = this;
    // Get the configured fund code
    const favorites: string[] = workspace
      .getConfiguration()
      .get('fund-watch.favorites'[]);// Get fund data
		return fundApi([...favorites]).then(
      (results: FundInfo[]) = > results.sort(
      	(prev, next) = > (prev.changeRate >= next.changeRate ? 1 : -1) * order ) ); }}Copy the code

Beautify the format

Previously we implemented the UI by directly instantiating a TreeItem. Now we need to reconstruct a TreeItem.

import { workspace, TreeDataProvider, TreeItem } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // Other code omitted
  getTreeItem(info: FundInfo): FundItem {
    return newFundItem(info); }}Copy the code
// TreeItem
import { TreeItem } from 'vscode';

export default class FundItem extends TreeItem {
  info: FundInfo;

  constructor(info: FundInfo) {
    const icon = Number(info.changeRate) >= 0 ? '📈' : '📉';

    // Add icon to make it more intuitive to know whether it is going up or down
    super(`${icon}${info.name}   ${info.changeRate}% `);

    let sliceName = info.name;
    if (sliceName.length > 8) {
      sliceName = `${sliceName.slice(0.8)}. `;
    }
    const tips = [
      ` code:${info.code}`.` name:${sliceName}`.` -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- `.'Net value of unit:${info.now}`.'Ups and downs:${info.changeRate}% `.'Ups and downs:${info.changeAmount}`.` yesterday closed:${info.lastClose}`,];this.info = info;
    // Tooltip Displays the content when the mouse is hovering over it
    this.tooltip = tips.join('\r\n'); }}Copy the code

Update the data

The TreeDataProvider needs to provide an onDidChangeTreeData property, which is an instance of EventEmitter, and then fire the EventEmitter instance to update the data. Each call to the refresh method is equivalent to calling the getChildren method again.

import { workspace, Event, EventEmitter, TreeDataProvider } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  private refreshEvent: EventEmitter<FundInfo | null> = new EventEmitter<FundInfo | null> (); readonly onDidChangeTreeData: Event<FundInfo |null> = this.refreshEvent.event;

  refresh() {
    // Update the view
    setTimeout(() = > {
      this.refreshEvent.fire(null);
    }, 200); }}Copy the code

Let’s go back to extension.ts and add a timer to keep the data updated periodically.

import { ExtensionContext, commands, window, workspace } from 'vscode'
import Provider from './data/Provider'

// Activate the plugin
export function activate(context: ExtensionContext) {
  // Get the interval configuration
  let interval = workspace.getConfiguration().get('fund-watch.interval'.2)
  if (interval < 2) {
    interval = 2
  }

  / / class
  const provider = new Provider()

  // Data registration
  window.registerTreeDataProvider('fund-list', provider)

  // Update regularly
  setInterval(() = > {
    provider.refresh()
  }, interval * 1000)}export function deactivate() {}

Copy the code

In addition to scheduled updates, we also need to provide the ability to manually update. Modify package.json to register commands.

{
  "contributes": {
		"commands": [{"command": "fund.refresh"."title": "Refresh"."icon": {
					"light": "images/light/refresh.svg"."dark": "images/dark/refresh.svg"}}]."menus": {
			"view/title": [{"when": "view == fund-list"."group": "navigation"."command": "fund.refresh"}]}}}Copy the code
  • commands: used to register commands, specify the name and icon of the command, and use the command to bind corresponding events in extension.
  • menus: used to mark the position of the command display;
    • when: Defines the view to be displayed. You can refer to the syntaxThe official documentation;
    • Group: defines groups of menus;
    • Command: Defines the event for which a command is invoked;

After you have configured the command, go back to extension.ts.

import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// Activate the plugin
export function activate(context: ExtensionContext) {
  let interval = workspace.getConfiguration().get('fund-watch.interval'.2);
  if (interval < 2) {
    interval = 2;
  }

  / / class
  const provider = new Provider();

  // Data registration
  window.registerTreeDataProvider('fund-list', provider);

  // Scheduled task
  setInterval(() = > {
    provider.refresh();
  }, interval * 1000);

  / / event
  context.subscriptions.push(
    commands.registerCommand('fund.refresh'.() = >{ provider.refresh(); })); }export function deactivate() {}

Copy the code

Now we can refresh manually.

The new fund

We have a new button that uses new funds.

{
  "contributes": {
		"commands": [{"command": "fund.add"."title": "New"."icon": {
          "light": "images/light/add.svg"."dark": "images/dark/add.svg"}}, {"command": "fund.refresh"."title": "Refresh"."icon": {
					"light": "images/light/refresh.svg"."dark": "images/dark/refresh.svg"}}]."menus": {
			"view/title": [{"command": "fund.add"."when": "view == fund-list"."group": "navigation"
        },
				{
					"when": "view == fund-list"."group": "navigation"."command": "fund.refresh"}]}}}Copy the code

Register events in extension.ts.

import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// Activate the plugin
export function activate(context: ExtensionContext) {
  // omit some code...
  
  / / class
  const provider = new Provider();

  / / event
  context.subscriptions.push(
    commands.registerCommand('fund.add'.() = > {
      provider.addFund();
    }),
    commands.registerCommand('fund.refresh'.() = >{ provider.refresh(); })); }export function deactivate() {}
Copy the code

Modify provider.ts to implement new functions.

import { workspace, Event, EventEmitter, TreeDataProvider } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // omit some code...

  // Update the configuration
  updateConfig(funds: string[]) {
    const config = workspace.getConfiguration();
    const favorites = Array.from(
      // Use Set to remove weights
      new Set([
        ...config.get('fund-watch.favorites', []),
        ...funds,
      ])
    );
    config.update('fund-watch.favorites', favorites, true);
  }

  async addFund() {
    // The input box is displayed
    const res = await window.showInputBox({
      value: ' '.valueSelection: [5, -1].prompt: 'Add funds to Optional'.placeHolder: 'Add Fund To Favorite'.validateInput: (inputCode: string) = > {
        const codeArray = inputCode.split(/[\W]/);
        const hasError = codeArray.some((code) = > {
          returncode ! = =' '&&!/^\d+$/.test(code);
        });
        return hasError ? 'Incorrect input of fund code' : null; }});if(!!!!! res) {const codeArray = res.split(/[\W]/) | | [];const result = await fundApi([...codeArray]);
      if (result && result.length > 0) {
        // Update only the code that can be requested properly
        const codes = result.map(i= > i.code);
        this.updateConfig(codes);
        this.refresh();
      } else {
        window.showWarningMessage('stocks not found'); }}}}Copy the code

Delete the fund

Finally, add a button to delete the fund.

{
	"contributes": {
		"commands": [{"command": "fund.item.remove"."title": "Delete"}]."menus": {
      // Put the button into the context
      "view/item/context": [{"command": "fund.item.remove"."when": "view == fund-list"."group": "inline"}]}}}Copy the code

Register events in extension.ts.

import { ExtensionContext, commands, window, workspace } from 'vscode';
import Provider from './Provider';

// Activate the plugin
export function activate(context: ExtensionContext) {
  // omit some code...
  
  / / class
  const provider = new Provider();

  / / event
  context.subscriptions.push(
    commands.registerCommand('fund.add'.() = > {
      provider.addFund();
    }),
    commands.registerCommand('fund.refresh'.() = > {
      provider.refresh();
    }),
    commands.registerCommand('fund.item.remove'.(fund) = > {
      const{ code } = fund; provider.removeConfig(code); provider.refresh(); })); }export function deactivate() {}
Copy the code

Modify provider.ts to implement new functions.

import { window, workspace, Event, EventEmitter, TreeDataProvider } from 'vscode';
import FundItem from './TreeItem';
import fundApi from './api';

export default class DataProvider implements TreeDataProvider<FundInfo> {
  // omit some code...

  // Delete the configuration
  removeConfig(code: string) {
    const config = workspace.getConfiguration();
    const favorites: string[] = [...config.get('fund-watch.favorites'And [])];const index = favorites.indexOf(code);
    if (index === -1) {
      return;
    }
    favorites.splice(index, 1);
    config.update('fund-watch.favorites', favorites, true); }}Copy the code

conclusion

The implementation process also encountered a lot of problems, encountered problems can be read VSCode plug-in Chinese documents. The plugin is now available on the VS Code plugin market, and those interested can download the plugin directly or download the full Code on Github.