Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 372295f

Browse files
committedMay 15, 2024·
feat: change login way and add tracking logic option
fix: resolve UAT issues fix: update action feat: add login progress fix: change function name
1 parent d713ad5 commit 372295f

17 files changed

+670
-237
lines changed
 

‎.vscode/settings.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
".vscode-test": true
1111
},
1212
"tslint.autoFixOnSave": true,
13-
"tslint.ignoreDefinitionFiles": true
14-
}
13+
"tslint.ignoreDefinitionFiles": true,
14+
"prettier.tabWidth": 4,
15+
"prettier.useTabs": false,
16+
"prettier.printWidth": 150
17+
}

‎README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- English Document | [中文文档](https://github.com/LeetCode-OpenSource/vscode-leetcode/blob/master/docs/README_zh-CN.md)
2424

2525
## ❗️ Attention ❗️- Workaround to login to LeetCode endpoint
26+
2627
> Note: If you are using `leetcode.cn`, you can just ignore this section.
2728
2829
Recently we observed that [the extension cannot login to leetcode.com endpoint anymore](https://github.com/LeetCode-OpenSource/vscode-leetcode/issues/478). The root cause of this issue is that leetcode.com changed its login mechanism and so far there is no ideal way to fix that issue.
@@ -32,9 +33,10 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
3233
> Note: If you want to use third-party login(**Recommended**), please make sure your account has been connected to the third-party. If you want to use `Cookie` login, click [here](https://github.com/LeetCode-OpenSource/vscode-leetcode/issues/478#issuecomment-564757098) to see the steps.
3334
3435
## Requirements
36+
3537
- [VS Code 1.30.1+](https://code.visualstudio.com/)
3638
- [Node.js 10+](https://nodejs.org)
37-
> NOTE: Please make sure that `Node` is in your `PATH` environment variable. You can also use the setting `leetcode.nodePath` to specify the location of your `Node.js` executable.
39+
> NOTE: Please make sure that `Node` is in your `PATH` environment variable. You can also use the setting `leetcode.nodePath` to specify the location of your `Node.js` executable.
3840
3941
## Quick Start
4042

@@ -43,6 +45,7 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
4345
## Features
4446

4547
### Sign In/Out
48+
4649
<p align="center">
4750
<img src="https://raw.githubusercontent.com/LeetCode-OpenSource/vscode-leetcode/master/docs/imgs/sign_in.png" alt="Sign in" />
4851
</p>
@@ -52,16 +55,19 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
5255
- You can also use the following command to sign in/out:
5356
- **LeetCode: Sign in**
5457
- **LeetCode: Sign out**
58+
5559
---
5660

5761
### Switch Endpoint
62+
5863
<p align="center">
5964
<img src="https://raw.githubusercontent.com/LeetCode-OpenSource/vscode-leetcode/master/docs/imgs/endpoint.png" alt="Switch Endpoint" />
6065
</p>
6166

6267
- By clicking the button ![btn_endpoint](https://raw.githubusercontent.com/LeetCode-OpenSource/vscode-leetcode/master/docs/imgs/btn_endpoint.png) at the **explorer's navigation bar**, you can switch between different endpoints.
6368

6469
- The supported endpoints are:
70+
6571
- **leetcode.com**
6672
- **leetcode.cn**
6773

@@ -70,6 +76,7 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
7076
---
7177

7278
### Pick a Problem
79+
7380
<p align="center">
7481
<img src="https://raw.githubusercontent.com/LeetCode-OpenSource/vscode-leetcode/master/docs/imgs/pick_problem.png" alt="Pick a Problem" />
7582
</p>
@@ -86,11 +93,13 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
8693
---
8794

8895
### Editor Shortcuts
96+
8997
<p align="center">
9098
<img src="https://raw.githubusercontent.com/LeetCode-OpenSource/vscode-leetcode/master/docs/imgs/shortcuts.png" alt="Editor Shortcuts" />
9199
</p>
92100

93101
- The extension supports 5 editor shortcuts (aka Code Lens):
102+
94103
- `Submit`: Submit your answer to LeetCode.
95104
- `Test`: Test your answer with customized test cases.
96105
- `Star/Unstar`: Star or unstar the current problem.
@@ -102,6 +111,7 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
102111
---
103112

104113
### Search problems by Keywords
114+
105115
<p align="center">
106116
<img src="https://raw.githubusercontent.com/LeetCode-OpenSource/vscode-leetcode/master/docs/imgs/search.png" alt="Search problems by Keywords" />
107117
</p>
@@ -111,19 +121,18 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
111121
---
112122

113123
### Manage Session
124+
114125
<p align="center">
115126
<img src="https://raw.githubusercontent.com/LeetCode-OpenSource/vscode-leetcode/master/docs/imgs/session.png" alt="Manage Session" />
116127
</p>
117128

118129
- To manage your LeetCode sessions, just clicking the `LeetCode: ***` at the bottom of the status bar. You can **switch** between sessions or **create**, **delete** a session.
119130

120-
121131
## Settings
122132

123133
| Setting Name | Description | Default Value |
124134
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
125135
| `leetcode.hideSolved` | Specify to hide the solved problems or not | `false` |
126-
| `leetcode.showLocked` | Specify to show the locked problems or not. Only Premium users could open the locked problems | `false` |
127136
| `leetcode.defaultLanguage` | Specify the default language used to solve the problem. Supported languages are: `bash`, `c`, `cpp`, `csharp`, `golang`, `java`, `javascript`, `kotlin`, `mysql`, `php`, `python`,`python3`,`ruby`,`rust`, `scala`, `swift`, `typescript` | `N/A` |
128137
| `leetcode.useWsl` | Specify whether to use WSL or not | `false` |
129138
| `leetcode.endpoint` | Specify the active endpoint. Supported endpoints are: `leetcode`, `leetcode-cn` | `leetcode` |
@@ -137,6 +146,7 @@ Thanks for [@yihong0618](https://github.com/yihong0618) provided a workaround wh
137146
| `leetcode.useEndpointTranslation` | Use endpoint's translation (if available) | `true` |
138147
| `leetcode.colorizeProblems` | Add difficulty badge and colorize problems files in explorer tree | `true` |
139148
| `leetcode.problems.sortStrategy` | Specify sorting strategy for problems list | `None` |
149+
| `leetcode.allowReportData` | Allow LeetCode to report anonymous usage data to improve the product. list | `true` |
140150

141151
## Want Help?
142152

‎docs/README_zh-CN.md

Lines changed: 30 additions & 19 deletions
Large diffs are not rendered by default.

‎package-lock.json

Lines changed: 74 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"name": "vscode-leetcode",
33
"displayName": "LeetCode",
44
"description": "Solve LeetCode problems in VS Code",
5-
"version": "0.18.1",
6-
"author": "Sheng Chen",
5+
"version": "0.18.2",
6+
"author": "LeetCode",
77
"publisher": "LeetCode",
88
"license": "MIT",
99
"icon": "resources/LeetCode.png",
@@ -182,15 +182,20 @@
182182
"when": "view == leetCodeExplorer",
183183
"group": "navigation@3"
184184
},
185+
{
186+
"command": "leetcode.signout",
187+
"when": "view == leetCodeExplorer",
188+
"group": "overflow@1"
189+
},
185190
{
186191
"command": "leetcode.pickOne",
187192
"when": "view == leetCodeExplorer",
188-
"group": "overflow@0"
193+
"group": "overflow@2"
189194
},
190195
{
191196
"command": "leetcode.problems.sort",
192197
"when": "view == leetCodeExplorer",
193-
"group": "overflow@1"
198+
"group": "overflow@3"
194199
}
195200
],
196201
"view/item/context": [
@@ -294,12 +299,6 @@
294299
"scope": "application",
295300
"description": "Hide solved problems."
296301
},
297-
"leetcode.showLocked": {
298-
"type": "boolean",
299-
"default": false,
300-
"scope": "application",
301-
"description": "Show locked problems."
302-
},
303302
"leetcode.defaultLanguage": {
304303
"type": "string",
305304
"enum": [
@@ -700,6 +699,12 @@
700699
"Acceptance Rate (Descending)"
701700
],
702701
"description": "Sorting strategy for problems list."
702+
},
703+
"leetcode.allowReportData": {
704+
"type": "boolean",
705+
"default": true,
706+
"scope": "application",
707+
"description": "Allow LeetCode to report anonymous usage data to improve the product."
703708
}
704709
}
705710
}
@@ -723,6 +728,7 @@
723728
"typescript": "^4.3.2"
724729
},
725730
"dependencies": {
731+
"axios": "^1.6.8",
726732
"fs-extra": "^10.0.0",
727733
"highlight.js": "^10.7.2",
728734
"lodash": "^4.17.21",

‎src/commands/list.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) jdneo. All rights reserved.
22
// Licensed under the MIT license.
33

4-
import * as vscode from "vscode";
54
import { leetCodeExecutor } from "../leetCodeExecutor";
65
import { leetCodeManager } from "../leetCodeManager";
76
import { IProblem, ProblemState, UserStatus } from "../shared";
@@ -13,10 +12,9 @@ export async function listProblems(): Promise<IProblem[]> {
1312
if (leetCodeManager.getStatus() === UserStatus.SignedOut) {
1413
return [];
1514
}
16-
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
17-
const showLocked: boolean = !!leetCodeConfig.get<boolean>("showLocked");
15+
1816
const useEndpointTranslation: boolean = settingUtils.shouldUseEndpointTranslation();
19-
const result: string = await leetCodeExecutor.listProblems(showLocked, useEndpointTranslation);
17+
const result: string = await leetCodeExecutor.listProblems(true, useEndpointTranslation);
2018
const problems: IProblem[] = [];
2119
const lines: string[] = result.split("\n");
2220
const reg: RegExp = /^(.)\s(.{1,2})\s(.)\s\[\s*(\d*)\s*\]\s*(.*)\s*(Easy|Medium|Hard)\s*\((\s*\d+\.\d+ %)\)/;

‎src/commands/show.ts

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,30 @@ import { LeetCodeNode } from "../explorer/LeetCodeNode";
1010
import { leetCodeChannel } from "../leetCodeChannel";
1111
import { leetCodeExecutor } from "../leetCodeExecutor";
1212
import { leetCodeManager } from "../leetCodeManager";
13-
import { IProblem, IQuickItemEx, languages, ProblemState } from "../shared";
13+
import { Endpoint, IProblem, IQuickItemEx, languages, PREMIUM_URL_CN, PREMIUM_URL_GLOBAL, ProblemState } from "../shared";
1414
import { genFileExt, genFileName, getNodeIdFromFile } from "../utils/problemUtils";
1515
import * as settingUtils from "../utils/settingUtils";
1616
import { IDescriptionConfiguration } from "../utils/settingUtils";
17-
import { DialogOptions, DialogType, openSettingsEditor, promptForOpenOutputChannel, promptForSignIn, promptHintMessage } from "../utils/uiUtils";
17+
import {
18+
DialogOptions,
19+
DialogType,
20+
openSettingsEditor,
21+
openUrl,
22+
promptForOpenOutputChannel,
23+
promptForSignIn,
24+
promptHintMessage,
25+
} from "../utils/uiUtils";
1826
import { getActiveFilePath, selectWorkspaceFolder } from "../utils/workspaceUtils";
1927
import * as wsl from "../utils/wslUtils";
2028
import { leetCodePreviewProvider } from "../webview/leetCodePreviewProvider";
2129
import { leetCodeSolutionProvider } from "../webview/leetCodeSolutionProvider";
2230
import * as list from "./list";
31+
import { getLeetCodeEndpoint } from "./plugin";
32+
import { globalState } from "../globalState";
2333

2434
export async function previewProblem(input: IProblem | vscode.Uri, isSideMode: boolean = false): Promise<void> {
2535
let node: IProblem;
36+
2637
if (input instanceof vscode.Uri) {
2738
const activeFilePath: string = input.fsPath;
2839
const id: string = await getNodeIdFromFile(activeFilePath);
@@ -40,7 +51,14 @@ export async function previewProblem(input: IProblem | vscode.Uri, isSideMode: b
4051
isSideMode = true;
4152
} else {
4253
node = input;
54+
const { isPremium } = globalState.getUserStatus() ?? {};
55+
if (input.locked && !isPremium) {
56+
const url = getLeetCodeEndpoint() === Endpoint.LeetCode ? PREMIUM_URL_GLOBAL : PREMIUM_URL_CN;
57+
openUrl(url);
58+
return;
59+
}
4360
}
61+
4462
const needTranslation: boolean = settingUtils.shouldUseEndpointTranslation();
4563
const descString: string = await leetCodeExecutor.getDescription(node.id, needTranslation);
4664
leetCodePreviewProvider.show(descString, node, isSideMode);
@@ -64,13 +82,10 @@ export async function searchProblem(): Promise<void> {
6482
promptForSignIn();
6583
return;
6684
}
67-
const choice: IQuickItemEx<IProblem> | undefined = await vscode.window.showQuickPick(
68-
parseProblemsToPicks(list.listProblems()),
69-
{
70-
matchOnDetail: true,
71-
placeHolder: "Select one problem",
72-
},
73-
);
85+
const choice: IQuickItemEx<IProblem> | undefined = await vscode.window.showQuickPick(parseProblemsToPicks(list.listProblems()), {
86+
matchOnDetail: true,
87+
placeHolder: "Select one problem",
88+
});
7489
if (!choice) {
7590
return;
7691
}
@@ -79,11 +94,14 @@ export async function searchProblem(): Promise<void> {
7994

8095
export async function showSolution(input: LeetCodeNode | vscode.Uri): Promise<void> {
8196
let problemInput: string | undefined;
82-
if (input instanceof LeetCodeNode) { // Triggerred from explorer
97+
if (input instanceof LeetCodeNode) {
98+
// Triggerred from explorer
8399
problemInput = input.id;
84-
} else if (input instanceof vscode.Uri) { // Triggerred from Code Lens/context menu
100+
} else if (input instanceof vscode.Uri) {
101+
// Triggerred from Code Lens/context menu
85102
problemInput = `"${input.fsPath}"`;
86-
} else if (!input) { // Triggerred from command
103+
} else if (!input) {
104+
// Triggerred from command
87105
problemInput = await getActiveFilePath();
88106
}
89107

@@ -112,15 +130,20 @@ async function fetchProblemLanguage(): Promise<string | undefined> {
112130
if (defaultLanguage && languages.indexOf(defaultLanguage) < 0) {
113131
defaultLanguage = undefined;
114132
}
115-
const language: string | undefined = defaultLanguage || await vscode.window.showQuickPick(languages, { placeHolder: "Select the language you want to use", ignoreFocusOut: true });
133+
const language: string | undefined =
134+
defaultLanguage ||
135+
(await vscode.window.showQuickPick(languages, {
136+
placeHolder: "Select the language you want to use",
137+
ignoreFocusOut: true,
138+
}));
116139
// fire-and-forget default language query
117140
(async (): Promise<void> => {
118141
if (language && !defaultLanguage && leetCodeConfig.get<boolean>("hint.setDefaultLanguage")) {
119142
const choice: vscode.MessageItem | undefined = await vscode.window.showInformationMessage(
120143
`Would you like to set '${language}' as your default language?`,
121144
DialogOptions.yes,
122145
DialogOptions.no,
123-
DialogOptions.never,
146+
DialogOptions.never
124147
);
125148
if (choice === DialogOptions.yes) {
126149
leetCodeConfig.update("defaultLanguage", language, true /* UserSetting */);
@@ -149,10 +172,7 @@ async function showProblemInternal(node: IProblem): Promise<void> {
149172
.get<string>(`filePath.${language}.folder`, leetCodeConfig.get<string>(`filePath.default.folder`, ""))
150173
.trim();
151174
const fileName: string = leetCodeConfig
152-
.get<string>(
153-
`filePath.${language}.filename`,
154-
leetCodeConfig.get<string>(`filePath.default.filename`) || genFileName(node, language),
155-
)
175+
.get<string>(`filePath.${language}.filename`, leetCodeConfig.get<string>(`filePath.default.filename`) || genFileName(node, language))
156176
.trim();
157177

158178
let finalPath: string = path.join(workspaceFolder, fileFolder, fileName);
@@ -172,12 +192,15 @@ async function showProblemInternal(node: IProblem): Promise<void> {
172192

173193
await leetCodeExecutor.showProblem(node, language, finalPath, descriptionConfig.showInComment, needTranslation);
174194
const promises: any[] = [
175-
vscode.window.showTextDocument(vscode.Uri.file(finalPath), { preview: false, viewColumn: vscode.ViewColumn.One }),
195+
vscode.window.showTextDocument(vscode.Uri.file(finalPath), {
196+
preview: false,
197+
viewColumn: vscode.ViewColumn.One,
198+
}),
176199
promptHintMessage(
177200
"hint.commentDescription",
178201
'You can config how to show the problem description through "leetcode.showDescription".',
179202
"Open settings",
180-
(): Promise<any> => openSettingsEditor("leetcode.showDescription"),
203+
(): Promise<any> => openSettingsEditor("leetcode.showDescription")
181204
),
182205
];
183206
if (descriptionConfig.showInWebview) {
@@ -195,12 +218,17 @@ async function showDescriptionView(node: IProblem): Promise<void> {
195218
}
196219
async function parseProblemsToPicks(p: Promise<IProblem[]>): Promise<Array<IQuickItemEx<IProblem>>> {
197220
return new Promise(async (resolve: (res: Array<IQuickItemEx<IProblem>>) => void): Promise<void> => {
198-
const picks: Array<IQuickItemEx<IProblem>> = (await p).map((problem: IProblem) => Object.assign({}, {
199-
label: `${parseProblemDecorator(problem.state, problem.locked)}${problem.id}.${problem.name}`,
200-
description: "",
201-
detail: `AC rate: ${problem.passRate}, Difficulty: ${problem.difficulty}`,
202-
value: problem,
203-
}));
221+
const picks: Array<IQuickItemEx<IProblem>> = (await p).map((problem: IProblem) =>
222+
Object.assign(
223+
{},
224+
{
225+
label: `${parseProblemDecorator(problem.state, problem.locked)}${problem.id}.${problem.name}`,
226+
description: "",
227+
detail: `AC rate: ${problem.passRate}, Difficulty: ${problem.difficulty}`,
228+
value: problem,
229+
}
230+
)
231+
);
204232
resolve(picks);
205233
});
206234
}
@@ -266,14 +294,11 @@ async function resolveTagForProblem(problem: IProblem): Promise<string | undefin
266294
if (problem.tags.length === 1) {
267295
return problem.tags[0];
268296
}
269-
return await vscode.window.showQuickPick(
270-
problem.tags,
271-
{
272-
matchOnDetail: true,
273-
placeHolder: "Multiple tags available, please select one",
274-
ignoreFocusOut: true,
275-
},
276-
);
297+
return await vscode.window.showQuickPick(problem.tags, {
298+
matchOnDetail: true,
299+
placeHolder: "Multiple tags available, please select one",
300+
ignoreFocusOut: true,
301+
});
277302
}
278303

279304
async function resolveCompanyForProblem(problem: IProblem): Promise<string | undefined> {

‎src/explorer/LeetCodeTreeDataProvider.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { leetCodeManager } from "../leetCodeManager";
88
import { Category, defaultProblem, ProblemState } from "../shared";
99
import { explorerNodeManager } from "./explorerNodeManager";
1010
import { LeetCodeNode } from "./LeetCodeNode";
11+
import { globalState } from "../globalState";
1112

1213
export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCodeNode> {
13-
1414
private context: vscode.ExtensionContext;
1515

16-
private onDidChangeTreeDataEvent: vscode.EventEmitter<LeetCodeNode | undefined | null> = new vscode.EventEmitter<LeetCodeNode | undefined | null>();
16+
private onDidChangeTreeDataEvent: vscode.EventEmitter<LeetCodeNode | undefined | null> = new vscode.EventEmitter<
17+
LeetCodeNode | undefined | null
18+
>();
1719
// tslint:disable-next-line:member-ordering
1820
public readonly onDidChangeTreeData: vscode.Event<any> = this.onDidChangeTreeDataEvent.event;
1921

@@ -46,7 +48,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
4648
}
4749

4850
return {
49-
label: element.isProblem ? `[${element.id}] ${element.name}` : element.name,
51+
label: element.isProblem ? `[${element.id}] ${element.name}` + this.parsePremiumUnLockIconPath(element) : element.name,
5052
tooltip: this.getSubCategoryTooltip(element),
5153
collapsibleState: element.isProblem ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed,
5254
iconPath: this.parseIconPathFromProblemState(element),
@@ -59,16 +61,20 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
5961
public getChildren(element?: LeetCodeNode | undefined): vscode.ProviderResult<LeetCodeNode[]> {
6062
if (!leetCodeManager.getUser()) {
6163
return [
62-
new LeetCodeNode(Object.assign({}, defaultProblem, {
63-
id: "notSignIn",
64-
name: "Sign in to LeetCode",
65-
}), false),
64+
new LeetCodeNode(
65+
Object.assign({}, defaultProblem, {
66+
id: "notSignIn",
67+
name: "Sign in to LeetCode",
68+
}),
69+
false
70+
),
6671
];
6772
}
68-
if (!element) { // Root view
73+
if (!element) {
74+
// Root view
6975
return explorerNodeManager.getRootNodes();
7076
} else {
71-
switch (element.id) { // First-level
77+
switch (element.id) {
7278
case Category.All:
7379
return explorerNodeManager.getAllNodes();
7480
case Category.Favorite:
@@ -92,13 +98,14 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
9298
if (!element.isProblem) {
9399
return "";
94100
}
101+
const { isPremium } = globalState.getUserStatus() ?? {};
95102
switch (element.state) {
96103
case ProblemState.AC:
97104
return this.context.asAbsolutePath(path.join("resources", "check.png"));
98105
case ProblemState.NotAC:
99106
return this.context.asAbsolutePath(path.join("resources", "x.png"));
100107
case ProblemState.Unknown:
101-
if (element.locked) {
108+
if (element.locked && !isPremium) {
102109
return this.context.asAbsolutePath(path.join("resources", "lock.png"));
103110
}
104111
return this.context.asAbsolutePath(path.join("resources", "blank.png"));
@@ -107,6 +114,14 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
107114
}
108115
}
109116

117+
private parsePremiumUnLockIconPath(element: LeetCodeNode): string {
118+
const { isPremium } = globalState.getUserStatus() ?? {};
119+
if (isPremium && element.locked) {
120+
return " 🔓";
121+
}
122+
return "";
123+
}
124+
110125
private getSubCategoryTooltip(element: LeetCodeNode): string {
111126
// return '' unless it is a sub-category node
112127
if (element.isProblem || element.id === "ROOT" || element.id in Category) {
@@ -130,11 +145,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
130145
}
131146
}
132147

133-
return [
134-
`AC: ${acceptedNum}`,
135-
`Failed: ${failedNum}`,
136-
`Total: ${childernNodes.length}`,
137-
].join(os.EOL);
148+
return [`AC: ${acceptedNum}`, `Failed: ${failedNum}`, `Total: ${childernNodes.length}`].join(os.EOL);
138149
}
139150
}
140151

‎src/extension.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ import { leetCodePreviewProvider } from "./webview/leetCodePreviewProvider";
2424
import { leetCodeSolutionProvider } from "./webview/leetCodeSolutionProvider";
2525
import { leetCodeSubmissionProvider } from "./webview/leetCodeSubmissionProvider";
2626
import { markdownEngine } from "./webview/markdownEngine";
27+
import TrackData from "./utils/trackingUtils";
28+
import { globalState } from "./globalState";
2729

2830
export async function activate(context: vscode.ExtensionContext): Promise<void> {
2931
try {
30-
if (!await leetCodeExecutor.meetRequirements(context)) {
32+
if (!(await leetCodeExecutor.meetRequirements(context))) {
3133
throw new Error("The environment doesn't meet requirements.");
3234
}
3335

@@ -37,6 +39,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
3739
});
3840

3941
leetCodeTreeDataProvider.initialize(context);
42+
globalState.initialize(context);
4043

4144
context.subscriptions.push(
4245
leetCodeStatusBarController,
@@ -55,22 +58,51 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
5558
vscode.commands.registerCommand("leetcode.signin", () => leetCodeManager.signIn()),
5659
vscode.commands.registerCommand("leetcode.signout", () => leetCodeManager.signOut()),
5760
vscode.commands.registerCommand("leetcode.manageSessions", () => session.manageSessions()),
58-
vscode.commands.registerCommand("leetcode.previewProblem", (node: LeetCodeNode) => show.previewProblem(node)),
61+
vscode.commands.registerCommand("leetcode.previewProblem", (node: LeetCodeNode) => {
62+
TrackData.report({
63+
event_key: `vscode_open_problem`,
64+
type: "click",
65+
extra: JSON.stringify({
66+
problem_id: node.id,
67+
problem_name: node.name,
68+
}),
69+
});
70+
show.previewProblem(node);
71+
}),
5972
vscode.commands.registerCommand("leetcode.showProblem", (node: LeetCodeNode) => show.showProblem(node)),
6073
vscode.commands.registerCommand("leetcode.pickOne", () => show.pickOne()),
6174
vscode.commands.registerCommand("leetcode.searchProblem", () => show.searchProblem()),
6275
vscode.commands.registerCommand("leetcode.showSolution", (input: LeetCodeNode | vscode.Uri) => show.showSolution(input)),
6376
vscode.commands.registerCommand("leetcode.refreshExplorer", () => leetCodeTreeDataProvider.refresh()),
64-
vscode.commands.registerCommand("leetcode.testSolution", (uri?: vscode.Uri) => test.testSolution(uri)),
65-
vscode.commands.registerCommand("leetcode.submitSolution", (uri?: vscode.Uri) => submit.submitSolution(uri)),
77+
vscode.commands.registerCommand("leetcode.testSolution", (uri?: vscode.Uri) => {
78+
TrackData.report({
79+
event_key: `vscode_runCode`,
80+
type: "click",
81+
extra: JSON.stringify({
82+
path: uri?.path,
83+
}),
84+
});
85+
return test.testSolution(uri);
86+
}),
87+
vscode.commands.registerCommand("leetcode.submitSolution", (uri?: vscode.Uri) => {
88+
TrackData.report({
89+
event_key: `vscode_submit`,
90+
type: "click",
91+
extra: JSON.stringify({
92+
path: uri?.path,
93+
}),
94+
});
95+
return submit.submitSolution(uri);
96+
}),
6697
vscode.commands.registerCommand("leetcode.switchDefaultLanguage", () => switchDefaultLanguage()),
6798
vscode.commands.registerCommand("leetcode.addFavorite", (node: LeetCodeNode) => star.addFavorite(node)),
6899
vscode.commands.registerCommand("leetcode.removeFavorite", (node: LeetCodeNode) => star.removeFavorite(node)),
69-
vscode.commands.registerCommand("leetcode.problems.sort", () => plugin.switchSortingStrategy()),
100+
vscode.commands.registerCommand("leetcode.problems.sort", () => plugin.switchSortingStrategy())
70101
);
71102

72103
await leetCodeExecutor.switchEndpoint(plugin.getLeetCodeEndpoint());
73104
await leetCodeManager.getLoginStatus();
105+
vscode.window.registerUriHandler({ handleUri: leetCodeManager.handleUriSignIn });
74106
} catch (error) {
75107
leetCodeChannel.appendLine(error.toString());
76108
promptForOpenOutputChannel("Extension initialization failed. Please open output channel for details.", DialogType.error);

‎src/globalState.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) leo.zhao. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
import * as vscode from "vscode";
5+
6+
const CookieKey = "leetcode-cookie";
7+
const UserStatusKey = "leetcode-user-status";
8+
9+
export type UserDataType = {
10+
isSignedIn: boolean;
11+
isPremium: boolean;
12+
username: string;
13+
avatar: string;
14+
isVerified?: boolean;
15+
};
16+
17+
class GlobalState {
18+
private context: vscode.ExtensionContext;
19+
private _state: vscode.Memento;
20+
private _cookie: string;
21+
private _userStatus: UserDataType;
22+
23+
public initialize(context: vscode.ExtensionContext): void {
24+
this.context = context;
25+
this._state = this.context.globalState;
26+
}
27+
28+
public setCookie(cookie: string): any {
29+
this._cookie = cookie;
30+
return this._state.update(CookieKey, this._cookie);
31+
}
32+
public getCookie(): string | undefined {
33+
return this._cookie ?? this._state.get(CookieKey);
34+
}
35+
36+
public setUserStatus(userStatus: UserDataType): any {
37+
this._userStatus = userStatus;
38+
return this._state.update(UserStatusKey, this._userStatus);
39+
}
40+
41+
public getUserStatus(): UserDataType | undefined {
42+
return this._userStatus ?? this._state.get(UserStatusKey);
43+
}
44+
45+
public removeCookie(): void {
46+
this._state.update(CookieKey, undefined);
47+
}
48+
49+
public removeAll(): void {
50+
this._state.update(CookieKey, undefined);
51+
this._state.update(UserStatusKey, undefined);
52+
}
53+
}
54+
55+
export const globalState: GlobalState = new GlobalState();

‎src/leetCodeManager.ts

Lines changed: 75 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import { EventEmitter } from "events";
66
import * as vscode from "vscode";
77
import { leetCodeChannel } from "./leetCodeChannel";
88
import { leetCodeExecutor } from "./leetCodeExecutor";
9-
import { IQuickItemEx, loginArgsMapping, UserStatus } from "./shared";
9+
import { Endpoint, loginArgsMapping, urls, urlsCn, UserStatus } from "./shared";
1010
import { createEnvOption } from "./utils/cpUtils";
11-
import { DialogType, promptForOpenOutputChannel } from "./utils/uiUtils";
11+
import { DialogType, openUrl, promptForOpenOutputChannel } from "./utils/uiUtils";
1212
import * as wsl from "./utils/wslUtils";
13+
import { getLeetCodeEndpoint } from "./commands/plugin";
14+
import { globalState } from "./globalState";
15+
import { queryUserData } from "./request/query-user-data";
16+
import { parseQuery, sleep } from "./utils/toolUtils";
1317

1418
class LeetCodeManager extends EventEmitter {
1519
private currentUser: string | undefined;
@@ -21,6 +25,7 @@ class LeetCodeManager extends EventEmitter {
2125
super();
2226
this.currentUser = undefined;
2327
this.userStatus = UserStatus.SignedOut;
28+
this.handleUriSignIn = this.handleUriSignIn.bind(this);
2429
}
2530

2631
public async getLoginStatus(): Promise<void> {
@@ -31,118 +36,40 @@ class LeetCodeManager extends EventEmitter {
3136
} catch (error) {
3237
this.currentUser = undefined;
3338
this.userStatus = UserStatus.SignedOut;
39+
globalState.removeAll();
3440
} finally {
3541
this.emit("statusChanged");
3642
}
3743
}
3844

39-
public async signIn(): Promise<void> {
40-
const picks: Array<IQuickItemEx<string>> = [];
41-
picks.push(
42-
{
43-
label: "LeetCode Account",
44-
detail: "Use LeetCode account to login (US endpoint is not supported)",
45-
value: "LeetCode",
46-
},
47-
{
48-
label: "Third-Party: GitHub",
49-
detail: "Use GitHub account to login",
50-
value: "GitHub",
51-
},
52-
{
53-
label: "Third-Party: LinkedIn",
54-
detail: "Use LinkedIn account to login",
55-
value: "LinkedIn",
56-
},
57-
{
58-
label: "LeetCode Cookie",
59-
detail: "Use LeetCode cookie copied from browser to login",
60-
value: "Cookie",
61-
},
62-
);
63-
const choice: IQuickItemEx<string> | undefined = await vscode.window.showQuickPick(picks);
64-
if (!choice) {
65-
return;
66-
}
67-
const loginMethod: string = choice.value;
68-
const commandArg: string | undefined = loginArgsMapping.get(loginMethod);
69-
if (!commandArg) {
70-
throw new Error(`The login method "${loginMethod}" is not supported.`);
71-
}
72-
const isByCookie: boolean = loginMethod === "Cookie";
73-
const inMessage: string = isByCookie ? "sign in by cookie" : "sign in";
45+
public async handleUriSignIn(uri: vscode.Uri): Promise<void> {
7446
try {
75-
const userName: string | undefined = await new Promise(async (resolve: (res: string | undefined) => void, reject: (e: Error) => void): Promise<void> => {
76-
77-
const leetCodeBinaryPath: string = await leetCodeExecutor.getLeetCodeBinaryPath();
78-
79-
const childProc: cp.ChildProcess = wsl.useWsl()
80-
? cp.spawn("wsl", [leetCodeExecutor.node, leetCodeBinaryPath, "user", commandArg], { shell: true })
81-
: cp.spawn(leetCodeExecutor.node, [leetCodeBinaryPath, "user", commandArg], {
82-
shell: true,
83-
env: createEnvOption(),
84-
});
85-
86-
childProc.stdout?.on("data", async (data: string | Buffer) => {
87-
data = data.toString();
88-
leetCodeChannel.append(data);
89-
if (data.includes("twoFactorCode")) {
90-
const twoFactor: string | undefined = await vscode.window.showInputBox({
91-
prompt: "Enter two-factor code.",
92-
ignoreFocusOut: true,
93-
validateInput: (s: string): string | undefined => s && s.trim() ? undefined : "The input must not be empty",
94-
});
95-
if (!twoFactor) {
96-
childProc.kill();
97-
return resolve(undefined);
98-
}
99-
childProc.stdin?.write(`${twoFactor}\n`);
100-
}
101-
const successMatch: RegExpMatchArray | null = data.match(this.successRegex);
102-
if (successMatch && successMatch[1]) {
103-
childProc.stdin?.end();
104-
return resolve(successMatch[1]);
105-
} else if (data.match(this.failRegex)) {
106-
childProc.stdin?.end();
107-
return reject(new Error("Faile to login"));
108-
}
109-
});
110-
111-
childProc.stderr?.on("data", (data: string | Buffer) => leetCodeChannel.append(data.toString()));
112-
113-
childProc.on("error", reject);
114-
const name: string | undefined = await vscode.window.showInputBox({
115-
prompt: "Enter username or E-mail.",
116-
ignoreFocusOut: true,
117-
validateInput: (s: string): string | undefined => s && s.trim() ? undefined : "The input must not be empty",
118-
});
119-
if (!name) {
120-
childProc.kill();
121-
return resolve(undefined);
47+
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress: vscode.Progress<{}>) => {
48+
progress.report({ message: "Fetching user data..." });
49+
const queryParams = parseQuery(uri.query);
50+
const cookie = queryParams["cookie"];
51+
if (!cookie) {
52+
promptForOpenOutputChannel(`Failed to get cookie. Please log in again`, DialogType.error);
53+
return;
12254
}
123-
childProc.stdin?.write(`${name}\n`);
124-
const pwd: string | undefined = await vscode.window.showInputBox({
125-
prompt: isByCookie ? "Enter cookie" : "Enter password.",
126-
password: true,
127-
ignoreFocusOut: true,
128-
validateInput: (s: string): string | undefined => s ? undefined : isByCookie ? "Cookie must not be empty" : "Password must not be empty",
129-
});
130-
if (!pwd) {
131-
childProc.kill();
132-
return resolve(undefined);
55+
globalState.setCookie(cookie);
56+
const data = await queryUserData();
57+
globalState.setUserStatus(data);
58+
await this.setCookieToCli(cookie, data.username);
59+
if (data.username) {
60+
vscode.window.showInformationMessage(`Successfully ${data.username}.`);
61+
this.currentUser = data.username;
62+
this.userStatus = UserStatus.SignedIn;
63+
this.emit("statusChanged");
13364
}
134-
childProc.stdin?.write(`${pwd}\n`);
13565
});
136-
if (userName) {
137-
vscode.window.showInformationMessage(`Successfully ${inMessage}.`);
138-
this.currentUser = userName;
139-
this.userStatus = UserStatus.SignedIn;
140-
this.emit("statusChanged");
141-
}
14266
} catch (error) {
143-
promptForOpenOutputChannel(`Failed to ${inMessage}. Please open the output channel for details`, DialogType.error);
67+
promptForOpenOutputChannel(`Failed to log in. Please open the output channel for details`, DialogType.error);
14468
}
69+
}
14570

71+
public async signIn(): Promise<void> {
72+
openUrl(this.getAuthLoginUrl());
14673
}
14774

14875
public async signOut(): Promise<void> {
@@ -151,6 +78,7 @@ class LeetCodeManager extends EventEmitter {
15178
vscode.window.showInformationMessage("Successfully signed out.");
15279
this.currentUser = undefined;
15380
this.userStatus = UserStatus.SignedOut;
81+
globalState.removeAll();
15482
this.emit("statusChanged");
15583
} catch (error) {
15684
// swallow the error when sign out.
@@ -174,6 +102,51 @@ class LeetCodeManager extends EventEmitter {
174102

175103
return "Unknown";
176104
}
105+
106+
public getAuthLoginUrl(): string {
107+
switch (getLeetCodeEndpoint()) {
108+
case Endpoint.LeetCodeCN:
109+
return urlsCn.authLoginUrl;
110+
case Endpoint.LeetCode:
111+
default:
112+
return urls.authLoginUrl;
113+
}
114+
}
115+
116+
public setCookieToCli(cookie: string, name: string): Promise<void> {
117+
return new Promise(async (resolve: (res: void) => void, reject: (e: Error) => void) => {
118+
const leetCodeBinaryPath: string = await leetCodeExecutor.getLeetCodeBinaryPath();
119+
120+
const childProc: cp.ChildProcess = wsl.useWsl()
121+
? cp.spawn("wsl", [leetCodeExecutor.node, leetCodeBinaryPath, "user", loginArgsMapping.get("Cookie") ?? ""], {
122+
shell: true,
123+
})
124+
: cp.spawn(leetCodeExecutor.node, [leetCodeBinaryPath, "user", loginArgsMapping.get("Cookie") ?? ""], {
125+
shell: true,
126+
env: createEnvOption(),
127+
});
128+
129+
childProc.stdout?.on("data", async (data: string | Buffer) => {
130+
data = data.toString();
131+
leetCodeChannel.append(data);
132+
const successMatch: RegExpMatchArray | null = data.match(this.successRegex);
133+
if (successMatch && successMatch[1]) {
134+
childProc.stdin?.end();
135+
return resolve();
136+
} else if (data.match(this.failRegex)) {
137+
childProc.stdin?.end();
138+
return reject(new Error("Faile to login"));
139+
}
140+
});
141+
142+
childProc.stderr?.on("data", (data: string | Buffer) => leetCodeChannel.append(data.toString()));
143+
144+
childProc.on("error", reject);
145+
childProc.stdin?.write(`${name}\n`);
146+
await sleep(800);
147+
childProc.stdin?.write(`${cookie}\n`);
148+
});
149+
}
177150
}
178151

179152
export const leetCodeManager: LeetCodeManager = new LeetCodeManager();

‎src/request/query-user-data.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { UserDataType } from "../globalState";
2+
import { getUrl } from "../shared";
3+
import { LcAxios } from "../utils/httpUtils";
4+
5+
const graphqlStr = `
6+
query globalData {
7+
userStatus {
8+
isPremium
9+
isVerified
10+
username
11+
avatar
12+
isSignedIn
13+
}
14+
}
15+
`;
16+
17+
export const queryUserData = async (): Promise<UserDataType> => {
18+
return LcAxios(getUrl("userGraphql"), {
19+
method: "POST",
20+
data: {
21+
query: graphqlStr,
22+
variables: {},
23+
},
24+
}).then((res) => res.data.data.userStatus);
25+
};

‎src/shared.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export enum ProblemState {
6363
AC = 1,
6464
NotAC = 2,
6565
Unknown = 3,
66+
Locked = 4,
6667
}
6768

6869
export enum Endpoint {
@@ -102,11 +103,7 @@ export enum Category {
102103
Favorite = "Favorite",
103104
}
104105

105-
export const supportedPlugins: string[] = [
106-
"company",
107-
"solution.discuss",
108-
"leetcode.cn",
109-
];
106+
export const supportedPlugins: string[] = ["company", "solution.discuss", "leetcode.cn"];
110107

111108
export enum DescriptionConfiguration {
112109
InWebView = "In Webview",
@@ -124,3 +121,36 @@ export enum SortingStrategy {
124121
FrequencyAsc = "Frequency (Ascending)",
125122
FrequencyDesc = "Frequency (Descending)",
126123
}
124+
125+
export const PREMIUM_URL_CN = "https://leetcode.cn/premium-payment/?source=vscode";
126+
export const PREMIUM_URL_GLOBAL = "https://leetcode.com/subscribe/?ref=lp_pl&source=vscode";
127+
128+
export const urls = {
129+
// base urls
130+
base: "https://leetcode.com",
131+
graphql: "https://leetcode.com/graphql",
132+
userGraphql: "https://leetcode.com/graphql",
133+
login: "https://leetcode.com/accounts/login/",
134+
authLoginUrl: "https://leetcode.com/authorize-login/vscode/?path=leetcode.vscode-leetcode",
135+
};
136+
137+
export const urlsCn = {
138+
// base urls
139+
base: "https://leetcode.cn",
140+
graphql: "https://leetcode.cn/graphql",
141+
userGraphql: "https://leetcode.cn/graphql/noj-go/",
142+
login: "https://leetcode.cn/accounts/login/",
143+
authLoginUrl: "https://leetcode.cn/authorize-login/vscode/?path=leetcode.vscode-leetcode",
144+
};
145+
146+
export const getUrl = (key: string) => {
147+
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
148+
const point = leetCodeConfig.get<string>("endpoint", Endpoint.LeetCode);
149+
switch (point) {
150+
case Endpoint.LeetCodeCN:
151+
return urlsCn[key];
152+
case Endpoint.LeetCode:
153+
default:
154+
return urls[key];
155+
}
156+
};

‎src/utils/httpUtils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import axios, { AxiosRequestConfig, AxiosPromise } from "axios";
2+
import { omit } from "lodash";
3+
import { globalState } from "../globalState";
4+
import { DialogType, promptForOpenOutputChannel } from "./uiUtils";
5+
6+
const referer = "vscode-lc-extension";
7+
8+
export function LcAxios<T = any>(path: string, settings?: AxiosRequestConfig): AxiosPromise<T> {
9+
const cookie = globalState.getCookie();
10+
if (!cookie) {
11+
promptForOpenOutputChannel(
12+
`Failed to obtain the cookie. Please log in again.`,
13+
DialogType.error
14+
);
15+
return Promise.reject("Failed to obtain the cookie.");
16+
}
17+
return axios(path, {
18+
headers: {
19+
referer: referer,
20+
"content-type": "application/json",
21+
cookie,
22+
...(settings && settings.headers),
23+
},
24+
xsrfCookieName: "csrftoken",
25+
xsrfHeaderName: "X-CSRFToken",
26+
...(settings && omit(settings, "headers")),
27+
});
28+
}

‎src/utils/toolUtils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function sleep(ms) {
2+
return new Promise((resolve) => setTimeout(resolve, ms));
3+
}
4+
5+
export function parseQuery(query: string): { [key: string]: string } {
6+
const queryObject: { [key: string]: string } = {};
7+
8+
if (!query) {
9+
return queryObject;
10+
}
11+
12+
let keyValuePairs = query.split("&");
13+
keyValuePairs.forEach((pair) => {
14+
const firstEqualsIndex = pair.indexOf("=");
15+
if (firstEqualsIndex !== -1) {
16+
const key = pair.substring(0, firstEqualsIndex);
17+
const value = pair.substring(firstEqualsIndex + 1);
18+
queryObject[decodeURIComponent(key)] = decodeURIComponent(value);
19+
} else {
20+
// If no equals sign is found, treat the whole string as key with empty value
21+
queryObject[decodeURIComponent(pair)] = "";
22+
}
23+
});
24+
25+
return queryObject;
26+
}

‎src/utils/trackingUtils.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import * as vscode from "vscode";
2+
import axios from "axios";
3+
import { getLeetCodeEndpoint } from "../commands/plugin";
4+
import { Endpoint } from "../shared";
5+
import { leetCodeManager } from "../leetCodeManager";
6+
7+
const getTimeZone = (): string => {
8+
const endPoint: string = getLeetCodeEndpoint();
9+
if (endPoint === Endpoint.LeetCodeCN) {
10+
return "Asia/Shanghai";
11+
} else {
12+
return "UTC";
13+
}
14+
};
15+
16+
interface IReportData {
17+
event_key: string;
18+
type?: "click" | "expose" | string;
19+
anonymous_id?: string;
20+
tid?: number;
21+
ename?: string; // event name
22+
href?: string;
23+
referer?: string;
24+
extra?: string;
25+
target?: string;
26+
}
27+
28+
interface ITrackData {
29+
reportCache: IReportData[];
30+
isSubmit: boolean;
31+
report: (reportItems: IReportData | IReportData[]) => void;
32+
submitReport: (useSendBeason: boolean) => Promise<void>;
33+
reportUrl: string;
34+
}
35+
36+
const testReportUrl = "https://analysis.lingkou.xyz/i/event";
37+
const prodReportUrl = "https://analysis.leetcode.cn/i/event";
38+
39+
function getReportUrl() {
40+
if (process.env.NODE_ENV === "production") {
41+
return prodReportUrl;
42+
} else {
43+
return testReportUrl;
44+
}
45+
}
46+
47+
const _charStr = "abacdefghjklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=";
48+
49+
function RandomIndex(min: number, max: number, i: number) {
50+
let index = Math.floor(Math.random() * (max - min + 1) + min);
51+
const numStart = _charStr.length - 10;
52+
if (i === 0 && index >= numStart) {
53+
index = RandomIndex(min, max, i);
54+
}
55+
return index;
56+
}
57+
58+
function getRandomString(len: number) {
59+
const min = 0;
60+
const max = _charStr.length - 1;
61+
let _str = "";
62+
len = len || 15;
63+
for (let i = 0, index; i < len; i++) {
64+
index = RandomIndex(min, max, i);
65+
_str += _charStr[index];
66+
}
67+
return _str;
68+
}
69+
70+
function getAllowReportDataConfig() {
71+
const leetCodeConfig = vscode.workspace.getConfiguration("leetcode");
72+
const allowReportData = !!leetCodeConfig.get<boolean>("allowReportData");
73+
return allowReportData;
74+
}
75+
76+
class TrackData implements ITrackData {
77+
public reportCache: IReportData[] = [];
78+
79+
public isSubmit: boolean = false;
80+
81+
public reportUrl: string = getReportUrl();
82+
83+
private sendTimer: NodeJS.Timeout | undefined;
84+
85+
public report = (reportItems: IReportData | IReportData[]) => {
86+
if (!getAllowReportDataConfig()) return;
87+
88+
this.sendTimer && clearTimeout(this.sendTimer);
89+
90+
if (!Array.isArray(reportItems)) {
91+
reportItems = [reportItems];
92+
}
93+
let randomId = getRandomString(60);
94+
reportItems.forEach((item) => {
95+
this.reportCache.push({
96+
...item,
97+
referer: "vscode",
98+
target: leetCodeManager.getUser() ?? "",
99+
anonymous_id: item.anonymous_id ?? (randomId as string),
100+
});
101+
});
102+
this.sendTimer = setTimeout(this.submitReport, 800);
103+
};
104+
105+
public submitReport = async () => {
106+
if (!getAllowReportDataConfig()) return;
107+
const dataList = JSON.stringify(this.reportCache);
108+
109+
if (!this.reportCache.length || this.isSubmit) {
110+
return;
111+
}
112+
this.reportCache = [];
113+
try {
114+
this.isSubmit = true;
115+
axios.defaults.withCredentials = true;
116+
await axios.post(this.reportUrl, `dataList=${encodeURIComponent(dataList)}`, {
117+
headers: {
118+
"Content-Type": "application/x-www-form-urlencoded",
119+
"x-timezone": getTimeZone(),
120+
},
121+
});
122+
} catch (e) {
123+
this.reportCache = this.reportCache.concat(JSON.parse(dataList));
124+
} finally {
125+
this.isSubmit = false;
126+
}
127+
};
128+
}
129+
130+
export default new TrackData();

‎src/webview/leetCodePreviewProvider.ts

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { ILeetCodeWebviewOption, LeetCodeWebview } from "./LeetCodeWebview";
88
import { markdownEngine } from "./markdownEngine";
99

1010
class LeetCodePreviewProvider extends LeetCodeWebview {
11-
1211
protected readonly viewType: string = "leetcode.preview";
1312
private node: IProblem;
1413
private description: IDescription;
@@ -23,11 +22,6 @@ class LeetCodePreviewProvider extends LeetCodeWebview {
2322
this.node = node;
2423
this.sideMode = isSideMode;
2524
this.showWebviewInternal();
26-
// Comment out this operation since it sometimes may cause the webview become empty.
27-
// Waiting for the progress of the VS Code side issue: https://github.com/microsoft/vscode/issues/3742
28-
// if (this.sideMode) {
29-
// this.hideSideBar(); // For better view area
30-
// }
3125
}
3226

3327
protected getWebviewOption(): ILeetCodeWebviewOption {
@@ -46,7 +40,7 @@ class LeetCodePreviewProvider extends LeetCodeWebview {
4640
}
4741

4842
protected getWebviewContent(): string {
49-
const button: { element: string, script: string, style: string } = {
43+
const button: { element: string; script: string; style: string } = {
5044
element: `<button id="solve">Code Now</button>`,
5145
script: `const button = document.getElementById('solve');
5246
button.onclick = () => vscode.postMessage({
@@ -73,32 +67,26 @@ class LeetCodePreviewProvider extends LeetCodeWebview {
7367
};
7468
const { title, url, category, difficulty, likes, dislikes, body } = this.description;
7569
const head: string = markdownEngine.render(`# [${title}](${url})`);
76-
const info: string = markdownEngine.render([
77-
`| Category | Difficulty | Likes | Dislikes |`,
78-
`| :------: | :--------: | :---: | :------: |`,
79-
`| ${category} | ${difficulty} | ${likes} | ${dislikes} |`,
80-
].join("\n"));
70+
const info: string = markdownEngine.render(
71+
[
72+
`| Category | Difficulty | Likes | Dislikes |`,
73+
`| :------: | :--------: | :---: | :------: |`,
74+
`| ${category} | ${difficulty} | ${likes} | ${dislikes} |`,
75+
].join("\n")
76+
);
8177
const tags: string = [
8278
`<details>`,
8379
`<summary><strong>Tags</strong></summary>`,
84-
markdownEngine.render(
85-
this.description.tags
86-
.map((t: string) => `[\`${t}\`](https://leetcode.com/tag/${t})`)
87-
.join(" | "),
88-
),
80+
markdownEngine.render(this.description.tags.map((t: string) => `[\`${t}\`](${this.getTagLink(t)})`).join(" | ")),
8981
`</details>`,
9082
].join("\n");
9183
const companies: string = [
9284
`<details>`,
9385
`<summary><strong>Companies</strong></summary>`,
94-
markdownEngine.render(
95-
this.description.companies
96-
.map((c: string) => `\`${c}\``)
97-
.join(" | "),
98-
),
86+
markdownEngine.render(this.description.companies.map((c: string) => `\`${c}\``).join(" | ")),
9987
`</details>`,
10088
].join("\n");
101-
const links: string = markdownEngine.render(`[Discussion](${this.getDiscussionLink(url)}) | [Solution](${this.getSolutionLink(url)})`);
89+
const links: string = markdownEngine.render(`[Submissions](${this.getSubmissionsLink(url)}) | [Solution](${this.getSolutionsLink(url)})`);
10290
return `
10391
<!DOCTYPE html>
10492
<html>
@@ -149,18 +137,23 @@ class LeetCodePreviewProvider extends LeetCodeWebview {
149137

150138
private parseDescription(descString: string, problem: IProblem): IDescription {
151139
const [
152-
/* title */, ,
153-
url, ,
154-
/* tags */, ,
155-
/* langs */, ,
156-
category,
140+
,
141+
,
142+
/* title */ url,
143+
,
144+
,
145+
,
146+
,
147+
,
148+
/* tags */ /* langs */ category,
157149
difficulty,
158150
likes,
159151
dislikes,
160-
/* accepted */,
161-
/* submissions */,
162-
/* testcase */, ,
163-
...body
152+
,
153+
,
154+
,
155+
,
156+
/* accepted */ /* submissions */ /* testcase */ ...body
164157
] = descString.split("\n");
165158
return {
166159
title: problem.name,
@@ -175,19 +168,22 @@ class LeetCodePreviewProvider extends LeetCodeWebview {
175168
};
176169
}
177170

178-
private getDiscussionLink(url: string): string {
171+
private getTagLink(tag: string): string {
179172
const endPoint: string = getLeetCodeEndpoint();
180173
if (endPoint === Endpoint.LeetCodeCN) {
181-
return url.replace("/description/", "/comments/");
174+
return `https://leetcode.cn/tag/${tag}?source=vscode`;
182175
} else if (endPoint === Endpoint.LeetCode) {
183-
return url.replace("/description/", "/discuss/?currentPage=1&orderBy=most_votes&query=");
176+
return `https://leetcode.com/tag/${tag}?source=vscode`;
184177
}
185178

186-
return "https://leetcode.com";
179+
return "https://leetcode.com?source=vscode";
187180
}
188181

189-
private getSolutionLink(url: string): string {
190-
return url.replace("/description/", "/solution/");
182+
private getSolutionsLink(url: string): string {
183+
return url.replace("/description/", "/solutions/") + "?source=vscode";
184+
}
185+
private getSubmissionsLink(url: string): string {
186+
return url.replace("/description/", "/submissions/") + "?source=vscode";
191187
}
192188
}
193189

0 commit comments

Comments
 (0)
Please sign in to comment.