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.name
ใpublisher
: 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 sidebardebug
: displays in the debug sidebarscm
: 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:
getChildren
This method takes an element and returns a child of the element. If there is no element, it returns a child of the root node.getTreeItem
This method accepts an Element and returns the view’s single row of UI data, required for theTreeItem
Instantiate;
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.