Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: liushaowei123/vscode-leetcode
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: LeetCode-OpenSource/vscode-leetcode
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Mar 4, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ccb62f5 View commit details

Commits on Mar 11, 2019

  1. Copy the full SHA
    b85611b View commit details

Commits on Mar 12, 2019

  1. Copy the full SHA
    f77737d View commit details

Commits on Mar 13, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    be4f2d5 View commit details
  2. Add sponsor

    jdneo committed Mar 13, 2019
    Copy the full SHA
    9164acb View commit details
  3. Copy the full SHA
    9b33df0 View commit details

Commits on Mar 15, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0300764 View commit details

Commits on Mar 17, 2019

  1. Copy the full SHA
    dba4fdd View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    bfadbf8 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    54777e8 View commit details
  4. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    2409d47 View commit details
  5. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0fb736c View commit details

Commits on Mar 21, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5f895a2 View commit details
  2. Copy the full SHA
    0552af2 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    358bca3 View commit details

Commits on Mar 22, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d12faba View commit details

Commits on Mar 23, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    bcafa92 View commit details

Commits on Mar 26, 2019

  1. Copy the full SHA
    3028be4 View commit details

Commits on Mar 27, 2019

  1. Copy the full SHA
    4eefc70 View commit details

Commits on Mar 29, 2019

  1. Add bot

    jdneo committed Mar 29, 2019
    Copy the full SHA
    ab33742 View commit details

Commits on Apr 1, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    a6367f5 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c6c4e9c View commit details

Commits on Apr 2, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9838b5d View commit details
  2. Copy the full SHA
    d935977 View commit details
  3. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    43feaa3 View commit details

Commits on Apr 3, 2019

  1. Copy the full SHA
    0bec0c4 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ce2eb30 View commit details

Commits on Apr 4, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    1430ac4 View commit details

Commits on Apr 8, 2019

  1. Copy the full SHA
    a91c783 View commit details

Commits on Apr 9, 2019

  1. Copy the full SHA
    bd34e1d View commit details

Commits on Apr 10, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    6b90097 View commit details

Commits on Apr 11, 2019

  1. Copy the full SHA
    d57d3fb View commit details

Commits on Apr 14, 2019

  1. Copy the full SHA
    ecac298 View commit details

Commits on Apr 16, 2019

  1. Copy the full SHA
    781feb2 View commit details

Commits on Apr 17, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    b77d76a View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c7273a4 View commit details
  3. Copy the full SHA
    b90354e View commit details

Commits on Apr 18, 2019

  1. Copy the full SHA
    8ac0287 View commit details

Commits on Apr 19, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    8d3e09f View commit details

Commits on Apr 22, 2019

  1. Copy the full SHA
    52a84d3 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    9dbe4fd View commit details

Commits on Apr 27, 2019

  1. Copy the full SHA
    0109197 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    bcaac0a View commit details

Commits on Apr 28, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    98533c9 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ba756a1 View commit details
  3. Copy the full SHA
    cfed70a View commit details
  4. Set theme jekyll-theme-cayman

    jdneo committed Apr 28, 2019
    Copy the full SHA
    1161e81 View commit details

Commits on May 1, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    de5f70d View commit details

Commits on May 7, 2019

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e920316 View commit details
  2. Copy the full SHA
    65f41c0 View commit details
Showing with 6,420 additions and 2,865 deletions.
  1. +6 −0 .github/ISSUE_TEMPLATE/bug.md
  2. +5 −1 .github/ISSUE_TEMPLATE/question.md
  3. +4 −0 .github/no-response.yml
  4. +50 −0 .github/workflows/build.yml
  5. +0 −19 .travis.yml
  6. +6 −3 .vscode/settings.json
  7. +0 −1 .vscodeignore
  8. +22 −10 ACKNOWLEDGEMENTS.md
  9. +219 −42 CHANGELOG.md
  10. +2 −1 LICENSE
  11. +80 −54 README.md
  12. +1 −0 _config.yml
  13. +78 −54 docs/README_zh-CN.md
  14. BIN docs/gifs/demo.gif
  15. BIN docs/imgs/pick_problem.png
  16. BIN docs/imgs/shortcuts.png
  17. BIN docs/imgs/submit.png
  18. BIN docs/imgs/test.png
  19. +2,651 −2,024 package-lock.json
  20. +508 −56 package.json
  21. BIN resources/LeetCode.png
  22. +9 −1 resources/LeetCode.svg
  23. BIN resources/check.png
  24. BIN resources/dark/dislike.png
  25. +0 −6 resources/dark/endpoint.svg
  26. BIN resources/dark/like.png
  27. +0 −6 resources/dark/refresh.svg
  28. +0 −6 resources/dark/search.svg
  29. +0 −7 resources/dark/signin.svg
  30. BIN resources/light/dislike.png
  31. +0 −6 resources/light/endpoint.svg
  32. BIN resources/light/like.png
  33. +0 −6 resources/light/refresh.svg
  34. +0 −6 resources/light/search.svg
  35. +0 −7 resources/light/signin.svg
  36. BIN resources/lock.png
  37. BIN resources/x.png
  38. +0 −28 src/codeLensProvider.ts
  39. +32 −0 src/codelens/CodeLensController.ts
  40. +94 −0 src/codelens/CustomCodeLensProvider.ts
  41. +4 −4 src/commands/list.ts
  42. +37 −3 src/commands/plugin.ts
  43. +74 −17 src/commands/session.ts
  44. +246 −70 src/commands/show.ts
  45. +34 −0 src/commands/star.ts
  46. +6 −2 src/commands/submit.ts
  47. +3 −3 src/commands/test.ts
  48. +23 −3 src/explorer/LeetCodeNode.ts
  49. +62 −177 src/explorer/LeetCodeTreeDataProvider.ts
  50. +40 −0 src/explorer/LeetCodeTreeItemDecorationProvider.ts
  51. +203 −0 src/explorer/explorerNodeManager.ts
  52. +88 −34 src/extension.ts
  53. +55 −0 src/globalState.ts
  54. +145 −46 src/leetCodeExecutor.ts
  55. +140 −59 src/leetCodeManager.ts
  56. +0 −52 src/leetCodeResultProvider.ts
  57. +25 −0 src/request/query-user-data.ts
  58. +65 −0 src/shared.ts
  59. +12 −7 src/{leetCodeStatusBarItem.ts → statusbar/LeetCodeStatusBarItem.ts}
  60. +46 −0 src/statusbar/leetCodeStatusBarController.ts
  61. +13 −4 src/utils/cpUtils.ts
  62. +28 −0 src/utils/httpUtils.ts
  63. +22 −2 src/utils/problemUtils.ts
  64. +68 −0 src/utils/settingUtils.ts
  65. +26 −0 src/utils/toolUtils.ts
  66. +130 −0 src/utils/trackingUtils.ts
  67. +49 −3 src/utils/uiUtils.ts
  68. +95 −12 src/utils/workspaceUtils.ts
  69. +6 −2 src/utils/wslUtils.ts
  70. +82 −0 src/webview/LeetCodeWebview.ts
  71. +206 −0 src/webview/leetCodePreviewProvider.ts
  72. +94 −0 src/webview/leetCodeSolutionProvider.ts
  73. +106 −0 src/webview/leetCodeSubmissionProvider.ts
  74. +166 −0 src/webview/markdownEngine.ts
  75. +236 −0 thirdpartynotice.txt
  76. +18 −21 tslint.json
6 changes: 6 additions & 0 deletions .github/ISSUE_TEMPLATE/bug.md
Original file line number Diff line number Diff line change
@@ -15,6 +15,12 @@ Steps to reproduce the behavior.

A clear and concise description of what you expected to happen.

## Extension Output

Paste here the LeetCode extension log from output channel.

Guidance: Press `Ctrl+Shift+U`, and toggle the channel to `LeetCode`.

## Your Environment

- *os*:
6 changes: 5 additions & 1 deletion .github/ISSUE_TEMPLATE/question.md
Original file line number Diff line number Diff line change
@@ -3,7 +3,11 @@ name: 💬 Questions / Help
about: If you have questions, please check our documents first
---

Before you submit an question we recommend you to check out the [DOCUMENT](https://github.com/jdneo/vscode-leetcode/blob/master/README.md) and [TROUBLESHOOTING PAGE](https://github.com/jdneo/vscode-leetcode/wiki/Troubleshooting) first.
Before you submit an question we recommend you to check out the [DOCUMENT](https://github.com/LeetCode-OpenSource/vscode-leetcode/blob/master/README.md) first.

You can also find more information in:
- [TROUBLESHOOTING](https://github.com/LeetCode-OpenSource/vscode-leetcode/wiki/Troubleshooting)
- [FAQ](https://github.com/LeetCode-OpenSource/vscode-leetcode/wiki/FAQ)

## 💬 Questions and Help

4 changes: 4 additions & 0 deletions .github/no-response.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
daysUntilClose: 14
responseRequiredLabel: need more info
closeComment: >
This issue has been closed automatically because it needs more information and has not had recent activity. Please reach out if you have or find the answers we need so that we can investigate further.
50 changes: 50 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
linux:
name: Linux
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v2

- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: 14

- name: Install Node.js modules
run: npm install

- name: Lint
run: npm run lint

- name: VSCE Packge
run: npx vsce package

windows:
name: Windows
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v2

- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: 14

- name: Install Node.js modules
run: npm install

- name: Lint
run: npm run lint

- name: VSCE Packge
run: npx vsce package
19 changes: 0 additions & 19 deletions .travis.yml

This file was deleted.

9 changes: 6 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"editor.insertSpaces": true,
"editor.tabSize": 4,
"files.insertFinalNewline": true,
@@ -10,5 +10,8 @@
".vscode-test": true
},
"tslint.autoFixOnSave": true,
"tslint.ignoreDefinitionFiles": true
}
"tslint.ignoreDefinitionFiles": true,
"prettier.tabWidth": 4,
"prettier.useTabs": false,
"prettier.printWidth": 150
}
1 change: 0 additions & 1 deletion .vscodeignore
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ test/**
src/**
**/*.map
.gitignore
.travis.yml
package-lock.json
tsconfig.json
tslint.json
32 changes: 22 additions & 10 deletions ACKNOWLEDGEMENTS.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
## Contributors 🙏❤

A big thanks to the following individuals for contributing:

- [@TsFreddie](https://github.com/TsFreddie) for contributing [#19](https://github.com/jdneo/vscode-leetcode/pull/19).
- [@ntt2k](https://github.com/ntt2k) for contributing [#38](https://github.com/jdneo/vscode-leetcode/pull/38).
- [@purocean](https://github.com/purocean) for contributing [#46](https://github.com/jdneo/vscode-leetcode/pull/46)
- [@Xeonacid](https://github.com/Xeonacid) for contributing [#58](https://github.com/jdneo/vscode-leetcode/pull/58).
- [@Himself65](https://github.com/Himself65) for contributing [#61](https://github.com/jdneo/vscode-leetcode/pull/61)
- [@Vigilans](https://github.com/Vigilans) for contributing [#94](https://github.com/jdneo/vscode-leetcode/pull/94)
- [@ringcrl](https://github.com/ringcrl) for contributing [#123](https://github.com/jdneo/vscode-leetcode/pull/123)
- [@pujiaxun](https://github.com/pujiaxun) for contributing [#143](https://github.com/jdneo/vscode-leetcode/pull/143)
- [@edvardchen](https://github.com/edvardchen) for contributing [#147](https://github.com/jdneo/vscode-leetcode/pull/147)
- [@poppinlp](https://github.com/poppinlp) for contributing [#149](https://github.com/jdneo/vscode-leetcode/pull/149), [#171](https://github.com/jdneo/vscode-leetcode/pull/171), [#175](https://github.com/jdneo/vscode-leetcode/pull/175)
- [@JIEJIAN21](https://github.com/JIEJIAN21) - thanks for logo and icon design
- [@TsFreddie](https://github.com/TsFreddie)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=TsFreddie)
- [@ntt2k](https://github.com/ntt2k)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=ntt2k)
- [@purocean](https://github.com/purocean)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=purocean)
- [@Xeonacid](https://github.com/Xeonacid)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=Xeonacid)
- [@Himself65](https://github.com/Himself65)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=Himself65)
- [@Vigilans](https://github.com/Vigilans)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=Vigilans)
- [@ringcrl](https://github.com/ringcrl)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=ringcrl)
- [@pujiaxun](https://github.com/pujiaxun)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=pujiaxun)
- [@edvardchen](https://github.com/edvardchen)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=edvardchen)
- [@poppinlp](https://github.com/poppinlp)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=poppinlp)
- [@xuzaixian](https://github.com/xuzaixian)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=xuzaixian)
- [@ZainChen](https://github.com/ZainChen)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=ZainChen)
- [@houtianze](https://github.com/houtianze)[contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=houtianze)
- [@magic-akari](https://github.com/magic-akari) - [contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=magic-akari)
- [@SF-Zhou](https://github.com/SF-Zhou) - [contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=SF-Zhou)
- [@fuafa](https://github.com/fuafa) - [contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=fuafa)
- [@iFun](https://github.com/iFun) - [contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=iFun)
- [@hologerry](https://github.com/hologerry) - [contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=hologerry)
- [@yihong0618](https://github.com/yihong0618) - [contributions](https://github.com/LeetCode-OpenSource/vscode-leetcode/commits?author=yihong0618)
261 changes: 219 additions & 42 deletions CHANGELOG.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2018 jdneo
Copyright (c) 2020-present 力扣
Copyright (c) 2018-2019 jdneo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
134 changes: 80 additions & 54 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions _config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
theme: jekyll-theme-cayman
132 changes: 78 additions & 54 deletions docs/README_zh-CN.md

Large diffs are not rendered by default.

Binary file modified docs/gifs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/imgs/pick_problem.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/imgs/shortcuts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/imgs/submit.png
Binary file not shown.
Binary file removed docs/imgs/test.png
Binary file not shown.
4,675 changes: 2,651 additions & 2,024 deletions package-lock.json

Large diffs are not rendered by default.

564 changes: 508 additions & 56 deletions package.json

Large diffs are not rendered by default.

Binary file modified resources/LeetCode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion resources/LeetCode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified resources/check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/dark/dislike.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions resources/dark/endpoint.svg

This file was deleted.

Binary file added resources/dark/like.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions resources/dark/refresh.svg

This file was deleted.

6 changes: 0 additions & 6 deletions resources/dark/search.svg

This file was deleted.

7 changes: 0 additions & 7 deletions resources/dark/signin.svg

This file was deleted.

Binary file added resources/light/dislike.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions resources/light/endpoint.svg

This file was deleted.

Binary file added resources/light/like.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions resources/light/refresh.svg

This file was deleted.

6 changes: 0 additions & 6 deletions resources/light/search.svg

This file was deleted.

7 changes: 0 additions & 7 deletions resources/light/signin.svg

This file was deleted.

Binary file modified resources/lock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified resources/x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 0 additions & 28 deletions src/codeLensProvider.ts

This file was deleted.

32 changes: 32 additions & 0 deletions src/codelens/CodeLensController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { ConfigurationChangeEvent, Disposable, languages, workspace } from "vscode";
import { customCodeLensProvider, CustomCodeLensProvider } from "./CustomCodeLensProvider";

class CodeLensController implements Disposable {
private internalProvider: CustomCodeLensProvider;
private registeredProvider: Disposable | undefined;
private configurationChangeListener: Disposable;

constructor() {
this.internalProvider = customCodeLensProvider;

this.configurationChangeListener = workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
if (event.affectsConfiguration("leetcode.editor.shortcuts")) {
this.internalProvider.refresh();
}
}, this);

this.registeredProvider = languages.registerCodeLensProvider({ scheme: "file" }, this.internalProvider);
}

public dispose(): void {
if (this.registeredProvider) {
this.registeredProvider.dispose();
}
this.configurationChangeListener.dispose();
}
}

export const codeLensController: CodeLensController = new CodeLensController();
94 changes: 94 additions & 0 deletions src/codelens/CustomCodeLensProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import * as vscode from "vscode";
import { explorerNodeManager } from "../explorer/explorerNodeManager";
import { LeetCodeNode } from "../explorer/LeetCodeNode";
import { getEditorShortcuts } from "../utils/settingUtils";

export class CustomCodeLensProvider implements vscode.CodeLensProvider {

private onDidChangeCodeLensesEmitter: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();

get onDidChangeCodeLenses(): vscode.Event<void> {
return this.onDidChangeCodeLensesEmitter.event;
}

public refresh(): void {
this.onDidChangeCodeLensesEmitter.fire();
}

public provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult<vscode.CodeLens[]> {
const shortcuts: string[] = getEditorShortcuts();
if (!shortcuts) {
return;
}

const content: string = document.getText();
const matchResult: RegExpMatchArray | null = content.match(/@lc app=.* id=(.*) lang=.*/);
if (!matchResult) {
return undefined;
}
const nodeId: string | undefined = matchResult[1];
let node: LeetCodeNode | undefined;
if (nodeId) {
node = explorerNodeManager.getNodeById(nodeId);
}

let codeLensLine: number = document.lineCount - 1;
for (let i: number = document.lineCount - 1; i >= 0; i--) {
const lineContent: string = document.lineAt(i).text;
if (lineContent.indexOf("@lc code=end") >= 0) {
codeLensLine = i;
break;
}
}

const range: vscode.Range = new vscode.Range(codeLensLine, 0, codeLensLine, 0);
const codeLens: vscode.CodeLens[] = [];

if (shortcuts.indexOf("submit") >= 0) {
codeLens.push(new vscode.CodeLens(range, {
title: "Submit",
command: "leetcode.submitSolution",
arguments: [document.uri],
}));
}

if (shortcuts.indexOf("test") >= 0) {
codeLens.push(new vscode.CodeLens(range, {
title: "Test",
command: "leetcode.testSolution",
arguments: [document.uri],
}));
}

if (shortcuts.indexOf("star") >= 0 && node) {
codeLens.push(new vscode.CodeLens(range, {
title: node.isFavorite ? "Unstar" : "Star",
command: node.isFavorite ? "leetcode.removeFavorite" : "leetcode.addFavorite",
arguments: [node],
}));
}

if (shortcuts.indexOf("solution") >= 0) {
codeLens.push(new vscode.CodeLens(range, {
title: "Solution",
command: "leetcode.showSolution",
arguments: [document.uri],
}));
}

if (shortcuts.indexOf("description") >= 0) {
codeLens.push(new vscode.CodeLens(range, {
title: "Description",
command: "leetcode.previewProblem",
arguments: [document.uri],
}));
}

return codeLens;
}
}

export const customCodeLensProvider: CustomCodeLensProvider = new CustomCodeLensProvider();
8 changes: 4 additions & 4 deletions src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import * as vscode from "vscode";
import { leetCodeExecutor } from "../leetCodeExecutor";
import { leetCodeManager } from "../leetCodeManager";
import { IProblem, ProblemState, UserStatus } from "../shared";
import * as settingUtils from "../utils/settingUtils";
import { DialogType, promptForOpenOutputChannel } from "../utils/uiUtils";

export async function listProblems(): Promise<IProblem[]> {
try {
if (leetCodeManager.getStatus() === UserStatus.SignedOut) {
return [];
}
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
const showLocked: boolean = !!leetCodeConfig.get<boolean>("showLocked");
const result: string = await leetCodeExecutor.listProblems(showLocked);

const useEndpointTranslation: boolean = settingUtils.shouldUseEndpointTranslation();
const result: string = await leetCodeExecutor.listProblems(true, useEndpointTranslation);
const problems: IProblem[] = [];
const lines: string[] = result.split("\n");
const reg: RegExp = /^(.)\s(.{1,2})\s(.)\s\[\s*(\d*)\s*\]\s*(.*)\s*(Easy|Medium|Hard)\s*\((\s*\d+\.\d+ %)\)/;
40 changes: 37 additions & 3 deletions src/commands/plugin.ts
Original file line number Diff line number Diff line change
@@ -2,9 +2,10 @@
// Licensed under the MIT license.

import * as vscode from "vscode";
import { leetCodeTreeDataProvider } from "../explorer/LeetCodeTreeDataProvider";
import { leetCodeExecutor } from "../leetCodeExecutor";
import { IQuickItemEx } from "../shared";
import { Endpoint } from "../shared";
import { Endpoint, SortingStrategy } from "../shared";
import { DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils";
import { deleteCache } from "./cache";

@@ -20,13 +21,13 @@ export async function switchEndpoint(): Promise<void> {
},
{
label: `${isCnEnabled ? "$(check) " : ""}力扣`,
description: "leetcode-cn.com",
description: "leetcode.cn",
detail: `启用中国版 LeetCode`,
value: Endpoint.LeetCodeCN,
},
);
const choice: IQuickItemEx<string> | undefined = await vscode.window.showQuickPick(picks);
if (!choice) {
if (!choice || choice.value === getLeetCodeEndpoint()) {
return;
}
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
@@ -52,3 +53,36 @@ export function getLeetCodeEndpoint(): string {
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
return leetCodeConfig.get<string>("endpoint", Endpoint.LeetCode);
}

const SORT_ORDER: SortingStrategy[] = [
SortingStrategy.None,
SortingStrategy.AcceptanceRateAsc,
SortingStrategy.AcceptanceRateDesc,
];

export async function switchSortingStrategy(): Promise<void> {
const currentStrategy: SortingStrategy = getSortingStrategy();
const picks: Array<IQuickItemEx<string>> = [];
picks.push(
...SORT_ORDER.map((s: SortingStrategy) => {
return {
label: `${currentStrategy === s ? "$(check)" : " "} ${s}`,
value: s,
};
}),
);

const choice: IQuickItemEx<string> | undefined = await vscode.window.showQuickPick(picks);
if (!choice || choice.value === currentStrategy) {
return;
}

const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
await leetCodeConfig.update("problems.sortStrategy", choice.value, true);
await leetCodeTreeDataProvider.refresh();
}

export function getSortingStrategy(): SortingStrategy {
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
return leetCodeConfig.get<SortingStrategy>("problems.sortStrategy", SortingStrategy.None);
}
91 changes: 74 additions & 17 deletions src/commands/session.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import * as vscode from "vscode";
import { leetCodeExecutor } from "../leetCodeExecutor";
import { leetCodeManager } from "../leetCodeManager";
import { IQuickItemEx } from "../shared";
import { DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils";
import { DialogOptions, DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils";

export async function getSessionList(): Promise<ISession[]> {
const signInStatus: string | undefined = leetCodeManager.getUser();
@@ -32,48 +32,64 @@ export async function getSessionList(): Promise<ISession[]> {
return sessions;
}

export async function selectSession(): Promise<void> {
const choice: IQuickItemEx<string> | undefined = await vscode.window.showQuickPick(parseSessionsToPicks());
export async function manageSessions(): Promise<void> {
const choice: IQuickItemEx<ISession | string> | undefined = await vscode.window.showQuickPick(parseSessionsToPicks(true /* includeOperation */));
if (!choice || choice.description === "Active") {
return;
}
if (choice.value === ":createNewSession") {
await vscode.commands.executeCommand("leetcode.createSession");
if (choice.value === ":createSession") {
await createSession();
return;
}
if (choice.value === ":deleteSession") {
await deleteSession();
return;
}
try {
await leetCodeExecutor.enableSession(choice.value);
await leetCodeExecutor.enableSession((choice.value as ISession).id);
vscode.window.showInformationMessage(`Successfully switched to session '${choice.label}'.`);
await vscode.commands.executeCommand("leetcode.refreshExplorer");
} catch (error) {
await promptForOpenOutputChannel("Failed to switch session. Please open the output channel for details.", DialogType.error);
}
}

async function parseSessionsToPicks(): Promise<Array<IQuickItemEx<string>>> {
return new Promise(async (resolve: (res: Array<IQuickItemEx<string>>) => void): Promise<void> => {
async function parseSessionsToPicks(includeOperations: boolean = false): Promise<Array<IQuickItemEx<ISession | string>>> {
return new Promise(async (resolve: (res: Array<IQuickItemEx<ISession | string>>) => void): Promise<void> => {
try {
const sessions: ISession[] = await getSessionList();
const picks: Array<IQuickItemEx<string>> = sessions.map((s: ISession) => Object.assign({}, {
const picks: Array<IQuickItemEx<ISession | string>> = sessions.map((s: ISession) => Object.assign({}, {
label: `${s.active ? "$(check) " : ""}${s.name}`,
description: s.active ? "Active" : "",
detail: `AC Questions: ${s.acQuestions}, AC Submits: ${s.acSubmits}`,
value: s.id,
value: s,
}));
picks.push({
label: "$(plus) Create a new session",
description: "",
detail: "Click this item to create a new session",
value: ":createNewSession",
});

if (includeOperations) {
picks.push(...parseSessionManagementOperations());
}
resolve(picks);
} catch (error) {
return await promptForOpenOutputChannel("Failed to list sessions. Please open the output channel for details.", DialogType.error);
}
});
}

export async function createSession(): Promise<void> {
function parseSessionManagementOperations(): Array<IQuickItemEx<string>> {
return [{
label: "$(plus) Create a session",
description: "",
detail: "Click this item to create a session",
value: ":createSession",
}, {
label: "$(trashcan) Delete a session",
description: "",
detail: "Click this item to DELETE a session",
value: ":deleteSession",
}];
}

async function createSession(): Promise<void> {
const session: string | undefined = await vscode.window.showInputBox({
prompt: "Enter the new session name.",
validateInput: (s: string): string | undefined => s && s.trim() ? undefined : "Session name must not be empty",
@@ -89,6 +105,47 @@ export async function createSession(): Promise<void> {
}
}

async function deleteSession(): Promise<void> {
const choice: IQuickItemEx<ISession | string> | undefined = await vscode.window.showQuickPick(
parseSessionsToPicks(false /* includeOperation */),
{ placeHolder: "Please select the session you want to delete" },
);
if (!choice) {
return;
}

const selectedSession: ISession = choice.value as ISession;
if (selectedSession.active) {
vscode.window.showInformationMessage("Cannot delete an active session.");
return;
}

const action: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(
`This operation cannot be reverted. Are you sure to delete the session: ${selectedSession.name}?`,
DialogOptions.yes,
DialogOptions.no,
);
if (action !== DialogOptions.yes) {
return;
}

const confirm: string | undefined = await vscode.window.showInputBox({
prompt: "Enter 'yes' to confirm deleting the session",
validateInput: (value: string): string => {
if (value === "yes") {
return "";
} else {
return "Enter 'yes' to confirm";
}
},
});

if (confirm === "yes") {
await leetCodeExecutor.deleteSession(selectedSession.id);
vscode.window.showInformationMessage("The session has been successfully deleted.");
}
}

export interface ISession {
active: boolean;
id: string;
316 changes: 246 additions & 70 deletions src/commands/show.ts

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions src/commands/star.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { customCodeLensProvider } from "../codelens/CustomCodeLensProvider";
import { LeetCodeNode } from "../explorer/LeetCodeNode";
import { leetCodeTreeDataProvider } from "../explorer/LeetCodeTreeDataProvider";
import { leetCodeExecutor } from "../leetCodeExecutor";
import { hasStarShortcut } from "../utils/settingUtils";
import { DialogType, promptForOpenOutputChannel } from "../utils/uiUtils";

export async function addFavorite(node: LeetCodeNode): Promise<void> {
try {
await leetCodeExecutor.toggleFavorite(node, true);
await leetCodeTreeDataProvider.refresh();
if (hasStarShortcut()) {
customCodeLensProvider.refresh();
}
} catch (error) {
await promptForOpenOutputChannel("Failed to add the problem to favorite. Please open the output channel for details.", DialogType.error);
}
}

export async function removeFavorite(node: LeetCodeNode): Promise<void> {
try {
await leetCodeExecutor.toggleFavorite(node, false);
await leetCodeTreeDataProvider.refresh();
if (hasStarShortcut()) {
customCodeLensProvider.refresh();
}
} catch (error) {
await promptForOpenOutputChannel("Failed to remove the problem from favorite. Please open the output channel for details.", DialogType.error);
}
}
8 changes: 6 additions & 2 deletions src/commands/submit.ts
Original file line number Diff line number Diff line change
@@ -2,11 +2,12 @@
// Licensed under the MIT license.

import * as vscode from "vscode";
import { leetCodeTreeDataProvider } from "../explorer/LeetCodeTreeDataProvider";
import { leetCodeExecutor } from "../leetCodeExecutor";
import { leetCodeManager } from "../leetCodeManager";
import { leetCodeResultProvider } from "../leetCodeResultProvider";
import { DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils";
import { getActiveFilePath } from "../utils/workspaceUtils";
import { leetCodeSubmissionProvider } from "../webview/leetCodeSubmissionProvider";

export async function submitSolution(uri?: vscode.Uri): Promise<void> {
if (!leetCodeManager.getUser()) {
@@ -21,8 +22,11 @@ export async function submitSolution(uri?: vscode.Uri): Promise<void> {

try {
const result: string = await leetCodeExecutor.submitSolution(filePath);
await leetCodeResultProvider.show(result);
leetCodeSubmissionProvider.show(result);
} catch (error) {
await promptForOpenOutputChannel("Failed to submit the solution. Please open the output channel for details.", DialogType.error);
return;
}

leetCodeTreeDataProvider.refresh();
}
6 changes: 3 additions & 3 deletions src/commands/test.ts
Original file line number Diff line number Diff line change
@@ -5,12 +5,12 @@ import * as fse from "fs-extra";
import * as vscode from "vscode";
import { leetCodeExecutor } from "../leetCodeExecutor";
import { leetCodeManager } from "../leetCodeManager";
import { leetCodeResultProvider } from "../leetCodeResultProvider";
import { IQuickItemEx, UserStatus } from "../shared";
import { isWindows, usingCmd } from "../utils/osUtils";
import { DialogType, promptForOpenOutputChannel, showFileSelectDialog } from "../utils/uiUtils";
import { getActiveFilePath } from "../utils/workspaceUtils";
import * as wsl from "../utils/wslUtils";
import { leetCodeSubmissionProvider } from "../webview/leetCodeSubmissionProvider";

export async function testSolution(uri?: vscode.Uri): Promise<void> {
try {
@@ -65,7 +65,7 @@ export async function testSolution(uri?: vscode.Uri): Promise<void> {
}
break;
case ":file":
const testFile: vscode.Uri[] | undefined = await showFileSelectDialog();
const testFile: vscode.Uri[] | undefined = await showFileSelectDialog(filePath);
if (testFile && testFile.length) {
const input: string = (await fse.readFile(testFile[0].fsPath, "utf-8")).trim();
if (input) {
@@ -81,7 +81,7 @@ export async function testSolution(uri?: vscode.Uri): Promise<void> {
if (!result) {
return;
}
await leetCodeResultProvider.show(result);
leetCodeSubmissionProvider.show(result);
} catch (error) {
await promptForOpenOutputChannel("Failed to test the solution. Please open the output channel for details.", DialogType.error);
}
26 changes: 23 additions & 3 deletions src/explorer/LeetCodeNode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { Command, Uri } from "vscode";
import { IProblem, ProblemState } from "../shared";

export class LeetCodeNode {
constructor(private data: IProblem, private parentNodeName: string, private isProblemNode: boolean = true) { }

constructor(private data: IProblem, private isProblemNode: boolean = true) { }

public get locked(): boolean {
return this.data.locked;
@@ -45,7 +47,25 @@ export class LeetCodeNode {
return this.isProblemNode;
}

public get parentName(): string {
return this.parentNodeName;
public get previewCommand(): Command {
return {
title: "Preview Problem",
command: "leetcode.previewProblem",
arguments: [this],
};
}

public get acceptanceRate(): number {
return Number(this.passRate.slice(0, -1).trim());
}

public get uri(): Uri {
return Uri.from({
scheme: "leetcode",
authority: this.isProblem ? "problems" : "tree-node",
path: `/${this.id}`, // path must begin with slash /
query: `difficulty=${this.difficulty}`,
});
}

}
239 changes: 62 additions & 177 deletions src/explorer/LeetCodeTreeDataProvider.ts
Original file line number Diff line number Diff line change
@@ -4,38 +4,34 @@
import * as os from "os";
import * as path from "path";
import * as vscode from "vscode";
import * as list from "../commands/list";
import { leetCodeChannel } from "../leetCodeChannel";
import { leetCodeManager } from "../leetCodeManager";
import { Category, defaultProblem, IProblem, ProblemState } from "../shared";
import { getWorkspaceConfiguration } from "../utils/workspaceUtils";
import { Category, defaultProblem, ProblemState } from "../shared";
import { explorerNodeManager } from "./explorerNodeManager";
import { LeetCodeNode } from "./LeetCodeNode";
import { globalState } from "../globalState";

export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCodeNode> {
private context: vscode.ExtensionContext;

private treeData: {
Difficulty: Map<string, IProblem[]>,
Tag: Map<string, IProblem[]>,
Company: Map<string, IProblem[]>,
Favorite: IProblem[],
};

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

constructor(private context: vscode.ExtensionContext) { }
public initialize(context: vscode.ExtensionContext): void {
this.context = context;
}

public async refresh(): Promise<void> {
await this.getProblemData();
this.onDidChangeTreeDataEvent.fire();
await explorerNodeManager.refreshCache();
this.onDidChangeTreeDataEvent.fire(null);
}

public getTreeItem(element: LeetCodeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
if (element.id === "notSignIn") {
return {
label: element.name,
id: element.id,
collapsibleState: vscode.TreeItemCollapsibleState.None,
command: {
command: "leetcode.signin",
@@ -44,121 +40,72 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
};
}

const idPrefix: number = Date.now();
let contextValue: string;
if (element.isProblem) {
contextValue = element.isFavorite ? "problem-favorite" : "problem";
} else {
contextValue = element.id.toLowerCase();
}

return {
label: element.isProblem ? `[${element.id}] ${element.name}` : element.name,
label: element.isProblem ? `[${element.id}] ${element.name}` + this.parsePremiumUnLockIconPath(element) : element.name,
tooltip: this.getSubCategoryTooltip(element),
id: `${idPrefix}.${element.parentName}.${element.id}`,
collapsibleState: element.isProblem ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed,
contextValue: element.isProblem ? "problem" : element.id.toLowerCase(),
iconPath: this.parseIconPathFromProblemState(element),
command: element.isProblem ? element.previewCommand : undefined,
resourceUri: element.uri,
contextValue,
};
}

public getChildren(element?: LeetCodeNode | undefined): vscode.ProviderResult<LeetCodeNode[]> {
if (!leetCodeManager.getUser()) {
return [
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: "notSignIn",
name: "Sign in to LeetCode",
}), "ROOT", false),
new LeetCodeNode(
Object.assign({}, defaultProblem, {
id: "notSignIn",
name: "Sign in to LeetCode",
}),
false
),
];
}
if (!element) { // Root view
return [
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Difficulty,
name: Category.Difficulty,
}), "ROOT", false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Tag,
name: Category.Tag,
}), "ROOT", false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Company,
name: Category.Company,
}), "ROOT", false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Favorite,
name: Category.Favorite,
}), "ROOT", false),
];
if (!element) {
// Root view
return explorerNodeManager.getRootNodes();
} else {
switch (element.name) { // First-level
switch (element.id) {
case Category.All:
return explorerNodeManager.getAllNodes();
case Category.Favorite:
const nodes: IProblem[] = this.treeData[Category.Favorite];
return nodes.map((p: IProblem) => new LeetCodeNode(p, Category.Favorite));
return explorerNodeManager.getFavoriteNodes();
case Category.Difficulty:
return explorerNodeManager.getAllDifficultyNodes();
case Category.Tag:
return explorerNodeManager.getAllTagNodes();
case Category.Company:
return this.composeSubCategoryNodes(element);
default: // Second and lower levels
return element.isProblem ? [] : this.composeProblemNodes(element);
}
}
}

private async getProblemData(): Promise<void> {
// clear cache
this.treeData = {
Difficulty: new Map<string, IProblem[]>(),
Tag: new Map<string, IProblem[]>(),
Company: new Map<string, IProblem[]>(),
Favorite: [],
};
for (const problem of await list.listProblems()) {
// Add favorite problem, no matter whether it is solved.
if (problem.isFavorite) {
this.treeData[Category.Favorite].push(problem);
}
// Hide solved problem in other category.
if (problem.state === ProblemState.AC && getWorkspaceConfiguration().get<boolean>("hideSolved")) {
continue;
return explorerNodeManager.getAllCompanyNodes();
default:
if (element.isProblem) {
return [];
}
return explorerNodeManager.getChildrenNodesById(element.id);
}

this.addProblemToTreeData(problem);
}
}

private composeProblemNodes(node: LeetCodeNode): LeetCodeNode[] {
const map: Map<string, IProblem[]> | undefined = this.treeData[node.parentName];
if (!map) {
leetCodeChannel.appendLine(`Category: ${node.parentName} is not available.`);
return [];
}
const problems: IProblem[] = map.get(node.name) || [];
const problemNodes: LeetCodeNode[] = [];
for (const problem of problems) {
problemNodes.push(new LeetCodeNode(problem, node.name));
}
return problemNodes;
}

private composeSubCategoryNodes(node: LeetCodeNode): LeetCodeNode[] {
const category: Category = node.name as Category;
if (category === Category.Favorite) {
leetCodeChannel.appendLine("No sub-level for Favorite nodes");
return [];
}
const map: Map<string, IProblem[]> | undefined = this.treeData[category];
if (!map) {
leetCodeChannel.appendLine(`Category: ${category} is not available.`);
return [];
}
return this.getSubCategoryNodes(map, category);
}

private parseIconPathFromProblemState(element: LeetCodeNode): string {
if (!element.isProblem) {
return "";
}
const { isPremium } = globalState.getUserStatus() ?? {};
switch (element.state) {
case ProblemState.AC:
return this.context.asAbsolutePath(path.join("resources", "check.png"));
case ProblemState.NotAC:
return this.context.asAbsolutePath(path.join("resources", "x.png"));
case ProblemState.Unknown:
if (element.locked) {
if (element.locked && !isPremium) {
return this.context.asAbsolutePath(path.join("resources", "lock.png"));
}
return this.context.asAbsolutePath(path.join("resources", "blank.png"));
@@ -167,18 +114,26 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
}
}

private parsePremiumUnLockIconPath(element: LeetCodeNode): string {
const { isPremium } = globalState.getUserStatus() ?? {};
if (isPremium && element.locked) {
return " 🔓";
}
return "";
}

private getSubCategoryTooltip(element: LeetCodeNode): string {
// return '' unless it is a sub-category node
if (element.isProblem || !this.treeData[element.parentName]) {
if (element.isProblem || element.id === "ROOT" || element.id in Category) {
return "";
}

const problems: IProblem[] = this.treeData[element.parentName].get(element.id);
const childernNodes: LeetCodeNode[] = explorerNodeManager.getChildrenNodesById(element.id);

let acceptedNum: number = 0;
let failedNum: number = 0;
for (const prob of problems) {
switch (prob.state) {
for (const node of childernNodes) {
switch (node.state) {
case ProblemState.AC:
acceptedNum++;
break;
@@ -190,78 +145,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
}
}

return [
`AC: ${acceptedNum}`,
`Failed: ${failedNum}`,
`Total: ${problems.length}`,
].join(os.EOL);
}

private addProblemToTreeData(problem: IProblem): void {
this.putProblemToMap(this.treeData.Difficulty, problem.difficulty, problem);
for (const tag of problem.tags) {
this.putProblemToMap(this.treeData.Tag, this.beautifyCategoryName(tag), problem);
}
for (const company of problem.companies) {
this.putProblemToMap(this.treeData.Company, this.beautifyCategoryName(company), problem);
}
}

private putProblemToMap(map: Map<string, IProblem[]>, key: string, problem: IProblem): void {
const problems: IProblem[] | undefined = map.get(key);
if (problems) {
problems.push(problem);
} else {
map.set(key, [problem]);
}
}

private beautifyCategoryName(name: string): string {
return name.split("-").map((c: string) => c[0].toUpperCase() + c.slice(1)).join(" ");
}

private getSubCategoryNodes(map: Map<string, IProblem[]>, category: Category): LeetCodeNode[] {
const subCategoryNodes: LeetCodeNode[] = Array.from(map.keys()).map((subCategory: string) => {
return new LeetCodeNode(Object.assign({}, defaultProblem, {
id: subCategory,
name: subCategory,
}), category.toString(), false);
});
this.sortSubCategoryNodes(subCategoryNodes, category);
return subCategoryNodes;
}

private sortSubCategoryNodes(subCategoryNodes: LeetCodeNode[], category: Category): void {
switch (category) {
case Category.Difficulty:
subCategoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => {
function getValue(input: LeetCodeNode): number {
switch (input.name.toLowerCase()) {
case "easy":
return 1;
case "medium":
return 2;
case "hard":
return 3;
default:
return Number.MAX_SAFE_INTEGER;
}
}
return getValue(a) - getValue(b);
});
case Category.Tag:
case Category.Company:
subCategoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => {
if (a.name === "Unknown") {
return 1;
} else if (b.name === "Unknown") {
return -1;
} else {
return Number(a.name > b.name) - Number(a.name < b.name);
}
});
default:
break;
}
return [`AC: ${acceptedNum}`, `Failed: ${failedNum}`, `Total: ${childernNodes.length}`].join(os.EOL);
}
}

export const leetCodeTreeDataProvider: LeetCodeTreeDataProvider = new LeetCodeTreeDataProvider();
40 changes: 40 additions & 0 deletions src/explorer/LeetCodeTreeItemDecorationProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { URLSearchParams } from "url";
import { FileDecoration, FileDecorationProvider, ProviderResult, ThemeColor, Uri, workspace, WorkspaceConfiguration } from "vscode";

export class LeetCodeTreeItemDecorationProvider implements FileDecorationProvider {
private readonly DIFFICULTY_BADGE_LABEL: { [key: string]: string } = {
easy: "E",
medium: "M",
hard: "H",
};

private readonly ITEM_COLOR: { [key: string]: ThemeColor } = {
easy: new ThemeColor("charts.green"),
medium: new ThemeColor("charts.yellow"),
hard: new ThemeColor("charts.red"),
};

public provideFileDecoration(uri: Uri): ProviderResult<FileDecoration> {
if (!this.isDifficultyBadgeEnabled()) {
return;
}

if (uri.scheme !== "leetcode" && uri.authority !== "problems") {
return;
}

const params: URLSearchParams = new URLSearchParams(uri.query);
const difficulty: string = params.get("difficulty")!.toLowerCase();
return {
badge: this.DIFFICULTY_BADGE_LABEL[difficulty],
color: this.ITEM_COLOR[difficulty],
};
}

private isDifficultyBadgeEnabled(): boolean {
const configuration: WorkspaceConfiguration = workspace.getConfiguration();
return configuration.get<boolean>("leetcode.colorizeProblems", false);
}
}

export const leetCodeTreeItemDecorationProvider: LeetCodeTreeItemDecorationProvider = new LeetCodeTreeItemDecorationProvider();
203 changes: 203 additions & 0 deletions src/explorer/explorerNodeManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import * as _ from "lodash";
import { Disposable } from "vscode";
import * as list from "../commands/list";
import { getSortingStrategy } from "../commands/plugin";
import { Category, defaultProblem, ProblemState, SortingStrategy } from "../shared";
import { shouldHideSolvedProblem } from "../utils/settingUtils";
import { LeetCodeNode } from "./LeetCodeNode";

class ExplorerNodeManager implements Disposable {
private explorerNodeMap: Map<string, LeetCodeNode> = new Map<string, LeetCodeNode>();
private companySet: Set<string> = new Set<string>();
private tagSet: Set<string> = new Set<string>();

public async refreshCache(): Promise<void> {
this.dispose();
const shouldHideSolved: boolean = shouldHideSolvedProblem();
for (const problem of await list.listProblems()) {
if (shouldHideSolved && problem.state === ProblemState.AC) {
continue;
}
this.explorerNodeMap.set(problem.id, new LeetCodeNode(problem));
for (const company of problem.companies) {
this.companySet.add(company);
}
for (const tag of problem.tags) {
this.tagSet.add(tag);
}
}
}

public getRootNodes(): LeetCodeNode[] {
return [
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.All,
name: Category.All,
}), false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Difficulty,
name: Category.Difficulty,
}), false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Tag,
name: Category.Tag,
}), false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Company,
name: Category.Company,
}), false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: Category.Favorite,
name: Category.Favorite,
}), false),
];
}

public getAllNodes(): LeetCodeNode[] {
return this.applySortingStrategy(
Array.from(this.explorerNodeMap.values()),
);
}

public getAllDifficultyNodes(): LeetCodeNode[] {
const res: LeetCodeNode[] = [];
res.push(
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: `${Category.Difficulty}.Easy`,
name: "Easy",
}), false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: `${Category.Difficulty}.Medium`,
name: "Medium",
}), false),
new LeetCodeNode(Object.assign({}, defaultProblem, {
id: `${Category.Difficulty}.Hard`,
name: "Hard",
}), false),
);
this.sortSubCategoryNodes(res, Category.Difficulty);
return res;
}

public getAllCompanyNodes(): LeetCodeNode[] {
const res: LeetCodeNode[] = [];
for (const company of this.companySet.values()) {
res.push(new LeetCodeNode(Object.assign({}, defaultProblem, {
id: `${Category.Company}.${company}`,
name: _.startCase(company),
}), false));
}
this.sortSubCategoryNodes(res, Category.Company);
return res;
}

public getAllTagNodes(): LeetCodeNode[] {
const res: LeetCodeNode[] = [];
for (const tag of this.tagSet.values()) {
res.push(new LeetCodeNode(Object.assign({}, defaultProblem, {
id: `${Category.Tag}.${tag}`,
name: _.startCase(tag),
}), false));
}
this.sortSubCategoryNodes(res, Category.Tag);
return res;
}

public getNodeById(id: string): LeetCodeNode | undefined {
return this.explorerNodeMap.get(id);
}

public getFavoriteNodes(): LeetCodeNode[] {
const res: LeetCodeNode[] = [];
for (const node of this.explorerNodeMap.values()) {
if (node.isFavorite) {
res.push(node);
}
}
return this.applySortingStrategy(res);
}

public getChildrenNodesById(id: string): LeetCodeNode[] {
// The sub-category node's id is named as {Category.SubName}
const metaInfo: string[] = id.split(".");
const res: LeetCodeNode[] = [];
for (const node of this.explorerNodeMap.values()) {
switch (metaInfo[0]) {
case Category.Company:
if (node.companies.indexOf(metaInfo[1]) >= 0) {
res.push(node);
}
break;
case Category.Difficulty:
if (node.difficulty === metaInfo[1]) {
res.push(node);
}
break;
case Category.Tag:
if (node.tags.indexOf(metaInfo[1]) >= 0) {
res.push(node);
}
break;
default:
break;
}
}
return this.applySortingStrategy(res);
}

public dispose(): void {
this.explorerNodeMap.clear();
this.companySet.clear();
this.tagSet.clear();
}

private sortSubCategoryNodes(subCategoryNodes: LeetCodeNode[], category: Category): void {
switch (category) {
case Category.Difficulty:
subCategoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => {
function getValue(input: LeetCodeNode): number {
switch (input.name.toLowerCase()) {
case "easy":
return 1;
case "medium":
return 2;
case "hard":
return 3;
default:
return Number.MAX_SAFE_INTEGER;
}
}
return getValue(a) - getValue(b);
});
break;
case Category.Tag:
case Category.Company:
subCategoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => {
if (a.name === "Unknown") {
return 1;
} else if (b.name === "Unknown") {
return -1;
} else {
return Number(a.name > b.name) - Number(a.name < b.name);
}
});
break;
default:
break;
}
}

private applySortingStrategy(nodes: LeetCodeNode[]): LeetCodeNode[] {
const strategy: SortingStrategy = getSortingStrategy();
switch (strategy) {
case SortingStrategy.AcceptanceRateAsc: return nodes.sort((x: LeetCodeNode, y: LeetCodeNode) => Number(x.acceptanceRate) - Number(y.acceptanceRate));
case SortingStrategy.AcceptanceRateDesc: return nodes.sort((x: LeetCodeNode, y: LeetCodeNode) => Number(y.acceptanceRate) - Number(x.acceptanceRate));
default: return nodes;
}
}
}

export const explorerNodeManager: ExplorerNodeManager = new ExplorerNodeManager();
122 changes: 88 additions & 34 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -2,57 +2,111 @@
// Licensed under the MIT license.

import * as vscode from "vscode";
import { codeLensProvider } from "./codeLensProvider";
import { codeLensController } from "./codelens/CodeLensController";
import * as cache from "./commands/cache";
import { switchDefaultLanguage } from "./commands/language";
import * as plugin from "./commands/plugin";
import * as session from "./commands/session";
import * as show from "./commands/show";
import * as star from "./commands/star";
import * as submit from "./commands/submit";
import * as test from "./commands/test";
import { explorerNodeManager } from "./explorer/explorerNodeManager";
import { LeetCodeNode } from "./explorer/LeetCodeNode";
import { LeetCodeTreeDataProvider } from "./explorer/LeetCodeTreeDataProvider";
import { leetCodeTreeDataProvider } from "./explorer/LeetCodeTreeDataProvider";
import { leetCodeTreeItemDecorationProvider } from "./explorer/LeetCodeTreeItemDecorationProvider";
import { leetCodeChannel } from "./leetCodeChannel";
import { leetCodeExecutor } from "./leetCodeExecutor";
import { leetCodeManager } from "./leetCodeManager";
import { leetCodeResultProvider } from "./leetCodeResultProvider";
import { leetCodeStatusBarItem } from "./leetCodeStatusBarItem";
import { leetCodeStatusBarController } from "./statusbar/leetCodeStatusBarController";
import { DialogType, promptForOpenOutputChannel } from "./utils/uiUtils";
import { leetCodePreviewProvider } from "./webview/leetCodePreviewProvider";
import { leetCodeSolutionProvider } from "./webview/leetCodeSolutionProvider";
import { leetCodeSubmissionProvider } from "./webview/leetCodeSubmissionProvider";
import { markdownEngine } from "./webview/markdownEngine";
import TrackData from "./utils/trackingUtils";
import { globalState } from "./globalState";

export async function activate(context: vscode.ExtensionContext): Promise<void> {
if (!await leetCodeExecutor.meetRequirements()) {
return;
}
try {
if (!(await leetCodeExecutor.meetRequirements(context))) {
throw new Error("The environment doesn't meet requirements.");
}

leetCodeManager.on("statusChanged", () => {
leetCodeStatusBarItem.updateStatusBar(leetCodeManager.getStatus(), leetCodeManager.getUser());
leetCodeTreeDataProvider.refresh();
});
leetCodeManager.on("statusChanged", () => {
leetCodeStatusBarController.updateStatusBar(leetCodeManager.getStatus(), leetCodeManager.getUser());
leetCodeTreeDataProvider.refresh();
});

const leetCodeTreeDataProvider: LeetCodeTreeDataProvider = new LeetCodeTreeDataProvider(context);
leetCodeResultProvider.initialize(context);
leetCodeTreeDataProvider.initialize(context);
globalState.initialize(context);

context.subscriptions.push(
leetCodeStatusBarItem,
leetCodeChannel,
leetCodeResultProvider,
vscode.window.registerTreeDataProvider("leetCodeExplorer", leetCodeTreeDataProvider),
vscode.languages.registerCodeLensProvider({ scheme: "file" }, codeLensProvider),
vscode.commands.registerCommand("leetcode.deleteCache", () => cache.deleteCache()),
vscode.commands.registerCommand("leetcode.toggleLeetCodeCn", () => plugin.switchEndpoint()),
vscode.commands.registerCommand("leetcode.signin", () => leetCodeManager.signIn()),
vscode.commands.registerCommand("leetcode.signout", () => leetCodeManager.signOut()),
vscode.commands.registerCommand("leetcode.selectSessions", () => session.selectSession()),
vscode.commands.registerCommand("leetcode.createSession", () => session.createSession()),
vscode.commands.registerCommand("leetcode.showProblem", (node: LeetCodeNode) => show.showProblem(node)),
vscode.commands.registerCommand("leetcode.searchProblem", () => show.searchProblem()),
vscode.commands.registerCommand("leetcode.refreshExplorer", () => leetCodeTreeDataProvider.refresh()),
vscode.commands.registerCommand("leetcode.testSolution", (uri?: vscode.Uri) => test.testSolution(uri)),
vscode.commands.registerCommand("leetcode.submitSolution", (uri?: vscode.Uri) => submit.submitSolution(uri)),
vscode.commands.registerCommand("leetcode.switchDefaultLanguage", () => switchDefaultLanguage()),
);
context.subscriptions.push(
leetCodeStatusBarController,
leetCodeChannel,
leetCodePreviewProvider,
leetCodeSubmissionProvider,
leetCodeSolutionProvider,
leetCodeExecutor,
markdownEngine,
codeLensController,
explorerNodeManager,
vscode.window.registerFileDecorationProvider(leetCodeTreeItemDecorationProvider),
vscode.window.createTreeView("leetCodeExplorer", { treeDataProvider: leetCodeTreeDataProvider, showCollapseAll: true }),
vscode.commands.registerCommand("leetcode.deleteCache", () => cache.deleteCache()),
vscode.commands.registerCommand("leetcode.toggleLeetCodeCn", () => plugin.switchEndpoint()),
vscode.commands.registerCommand("leetcode.signin", () => leetCodeManager.signIn()),
vscode.commands.registerCommand("leetcode.signout", () => leetCodeManager.signOut()),
vscode.commands.registerCommand("leetcode.manageSessions", () => session.manageSessions()),
vscode.commands.registerCommand("leetcode.previewProblem", (node: LeetCodeNode) => {
TrackData.report({
event_key: `vscode_open_problem`,
type: "click",
extra: JSON.stringify({
problem_id: node.id,
problem_name: node.name,
}),
});
show.previewProblem(node);
}),
vscode.commands.registerCommand("leetcode.showProblem", (node: LeetCodeNode) => show.showProblem(node)),
vscode.commands.registerCommand("leetcode.pickOne", () => show.pickOne()),
vscode.commands.registerCommand("leetcode.searchProblem", () => show.searchProblem()),
vscode.commands.registerCommand("leetcode.showSolution", (input: LeetCodeNode | vscode.Uri) => show.showSolution(input)),
vscode.commands.registerCommand("leetcode.refreshExplorer", () => leetCodeTreeDataProvider.refresh()),
vscode.commands.registerCommand("leetcode.testSolution", (uri?: vscode.Uri) => {
TrackData.report({
event_key: `vscode_runCode`,
type: "click",
extra: JSON.stringify({
path: uri?.path,
}),
});
return test.testSolution(uri);
}),
vscode.commands.registerCommand("leetcode.submitSolution", (uri?: vscode.Uri) => {
TrackData.report({
event_key: `vscode_submit`,
type: "click",
extra: JSON.stringify({
path: uri?.path,
}),
});
return submit.submitSolution(uri);
}),
vscode.commands.registerCommand("leetcode.switchDefaultLanguage", () => switchDefaultLanguage()),
vscode.commands.registerCommand("leetcode.addFavorite", (node: LeetCodeNode) => star.addFavorite(node)),
vscode.commands.registerCommand("leetcode.removeFavorite", (node: LeetCodeNode) => star.removeFavorite(node)),
vscode.commands.registerCommand("leetcode.problems.sort", () => plugin.switchSortingStrategy())
);

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

export function deactivate(): void {
55 changes: 55 additions & 0 deletions src/globalState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) leo.zhao. All rights reserved.
// Licensed under the MIT license.

import * as vscode from "vscode";

const CookieKey = "leetcode-cookie";
const UserStatusKey = "leetcode-user-status";

export type UserDataType = {
isSignedIn: boolean;
isPremium: boolean;
username: string;
avatar: string;
isVerified?: boolean;
};

class GlobalState {
private context: vscode.ExtensionContext;
private _state: vscode.Memento;
private _cookie: string;
private _userStatus: UserDataType;

public initialize(context: vscode.ExtensionContext): void {
this.context = context;
this._state = this.context.globalState;
}

public setCookie(cookie: string): any {
this._cookie = cookie;
return this._state.update(CookieKey, this._cookie);
}
public getCookie(): string | undefined {
return this._cookie ?? this._state.get(CookieKey);
}

public setUserStatus(userStatus: UserDataType): any {
this._userStatus = userStatus;
return this._state.update(UserStatusKey, this._userStatus);
}

public getUserStatus(): UserDataType | undefined {
return this._userStatus ?? this._state.get(UserStatusKey);
}

public removeCookie(): void {
this._state.update(CookieKey, undefined);
}

public removeAll(): void {
this._state.update(CookieKey, undefined);
this._state.update(UserStatusKey, undefined);
}
}

export const globalState: GlobalState = new GlobalState();
191 changes: 145 additions & 46 deletions src/leetCodeExecutor.ts
Original file line number Diff line number Diff line change
@@ -3,43 +3,58 @@

import * as cp from "child_process";
import * as fse from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as requireFromString from "require-from-string";
import * as vscode from "vscode";
import { Endpoint, IProblem } from "./shared";
import { ExtensionContext } from "vscode";
import { ConfigurationChangeEvent, Disposable, MessageItem, window, workspace, WorkspaceConfiguration } from "vscode";
import { Endpoint, IProblem, leetcodeHasInited, supportedPlugins } from "./shared";
import { executeCommand, executeCommandWithProgress } from "./utils/cpUtils";
import { genFileName } from "./utils/problemUtils";
import { DialogOptions, openUrl } from "./utils/uiUtils";
import * as wsl from "./utils/wslUtils";
import { toWslPath, useWsl } from "./utils/wslUtils";

class LeetCodeExecutor {
class LeetCodeExecutor implements Disposable {
private leetCodeRootPath: string;
private leetCodeRootPathInWsl: string;
private nodeExecutable: string;
private configurationChangeListener: Disposable;

constructor() {
this.leetCodeRootPath = path.join(__dirname, "..", "..", "node_modules", "vsc-leetcode-cli");
this.leetCodeRootPathInWsl = "";
this.nodeExecutable = this.getNodePath();
this.configurationChangeListener = workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
if (event.affectsConfiguration("leetcode.nodePath")) {
this.nodeExecutable = this.getNodePath();
}
}, this);
}

public async getLeetCodeRootPath(): Promise<string> { // not wrapped by ""
public async getLeetCodeBinaryPath(): Promise<string> {
if (wsl.useWsl()) {
if (!this.leetCodeRootPathInWsl) {
this.leetCodeRootPathInWsl = `${await wsl.toWslPath(this.leetCodeRootPath)}`;
}
return `${this.leetCodeRootPathInWsl}`;
return `${await wsl.toWslPath(`"${path.join(this.leetCodeRootPath, "bin", "leetcode")}"`)}`;
}
return `${this.leetCodeRootPath}`;
return `"${path.join(this.leetCodeRootPath, "bin", "leetcode")}"`;
}

public async getLeetCodeBinaryPath(): Promise<string> { // wrapped by ""
return `"${path.join(await this.getLeetCodeRootPath(), "bin", "leetcode")}"`;
}

public async meetRequirements(): Promise<boolean> {
public async meetRequirements(context: ExtensionContext): Promise<boolean> {
const hasInited: boolean | undefined = context.globalState.get(leetcodeHasInited);
if (!hasInited) {
await this.removeOldCache();
}
if (this.nodeExecutable !== "node") {
if (!await fse.pathExists(this.nodeExecutable)) {
throw new Error(`The Node.js executable does not exist on path ${this.nodeExecutable}`);
}
// Wrap the executable with "" to avoid space issue in the path.
this.nodeExecutable = `"${this.nodeExecutable}"`;
if (useWsl()) {
this.nodeExecutable = await toWslPath(this.nodeExecutable);
}
}
try {
await this.executeCommandEx("node", ["-v"]);
await this.executeCommandEx(this.nodeExecutable, ["-v"]);
} catch (error) {
const choice: vscode.MessageItem | undefined = await vscode.window.showErrorMessage(
const choice: MessageItem | undefined = await window.showErrorMessage(
"LeetCode extension needs Node.js installed in environment path",
DialogOptions.open,
);
@@ -48,81 +63,144 @@ class LeetCodeExecutor {
}
return false;
}
try { // Check company plugin
await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", "company"]);
} catch (error) { // Download company plugin and activate
await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-i", "company"]);
for (const plugin of supportedPlugins) {
try { // Check plugin
await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "plugin", "-e", plugin]);
} catch (error) { // Remove old cache that may cause the error download plugin and activate
await this.removeOldCache();
await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "plugin", "-i", plugin]);
}
}
// Set the global state HasInited true to skip delete old cache after init
context.globalState.update(leetcodeHasInited, true);
return true;
}

public async deleteCache(): Promise<string> {
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "cache", "-d"]);
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "cache", "-d"]);
}

public async getUserInfo(): Promise<string> {
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "user"]);
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "user"]);
}

public async signOut(): Promise<string> {
return await await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "user", "-L"]);
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "user", "-L"]);
}

public async listProblems(showLocked: boolean): Promise<string> {
return await this.executeCommandEx("node", showLocked ?
[await this.getLeetCodeBinaryPath(), "list"] :
[await this.getLeetCodeBinaryPath(), "list", "-q", "L"],
);
public async listProblems(showLocked: boolean, needTranslation: boolean): Promise<string> {
const cmd: string[] = [await this.getLeetCodeBinaryPath(), "list"];
if (!needTranslation) {
cmd.push("-T"); // use -T to prevent translation
}
if (!showLocked) {
cmd.push("-q");
cmd.push("L");
}
return await this.executeCommandEx(this.nodeExecutable, cmd);
}

public async showProblem(node: IProblem, language: string, outDir: string): Promise<string> {
const fileName: string = genFileName(node, language);
const filePath: string = path.join(outDir, fileName);
public async showProblem(problemNode: IProblem, language: string, filePath: string, showDescriptionInComment: boolean = false, needTranslation: boolean): Promise<void> {
const templateType: string = showDescriptionInComment ? "-cx" : "-c";
const cmd: string[] = [await this.getLeetCodeBinaryPath(), "show", problemNode.id, templateType, "-l", language];

if (!needTranslation) {
cmd.push("-T"); // use -T to force English version
}

if (!await fse.pathExists(filePath)) {
const codeTemplate: string = await this.executeCommandWithProgressEx("Fetching problem data...", "node", [await this.getLeetCodeBinaryPath(), "show", node.id, "-cx", "-l", language]);
await fse.createFile(filePath);
const codeTemplate: string = await this.executeCommandWithProgressEx("Fetching problem data...", this.nodeExecutable, cmd);
await fse.writeFile(filePath, codeTemplate);
}
}

return filePath;
/**
* This function returns solution of a problem identified by input
*
* @remarks
* Even though this function takes the needTranslation flag, it is important to note
* that as of vsc-leetcode-cli 2.8.0, leetcode-cli doesn't support querying solution
* on CN endpoint yet. So this flag doesn't have any effect right now.
*
* @param input - parameter to pass to cli that can identify a problem
* @param language - the source code language of the solution desired
* @param needTranslation - whether or not to use endPoint translation on solution query
* @returns promise of the solution string
*/
public async showSolution(input: string, language: string, needTranslation: boolean): Promise<string> {
// solution don't support translation
const cmd: string[] = [await this.getLeetCodeBinaryPath(), "show", input, "--solution", "-l", language];
if (!needTranslation) {
cmd.push("-T");
}
const solution: string = await this.executeCommandWithProgressEx("Fetching top voted solution from discussions...", this.nodeExecutable, cmd);
return solution;
}

public async getDescription(problemNodeId: string, needTranslation: boolean): Promise<string> {
const cmd: string[] = [await this.getLeetCodeBinaryPath(), "show", problemNodeId, "-x"];
if (!needTranslation) {
cmd.push("-T");
}
return await this.executeCommandWithProgressEx("Fetching problem description...", this.nodeExecutable, cmd);
}

public async listSessions(): Promise<string> {
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session"]);
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "session"]);
}

public async enableSession(name: string): Promise<string> {
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session", "-e", name]);
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "session", "-e", name]);
}

public async createSession(name: string): Promise<string> {
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session", "-c", name]);
public async createSession(id: string): Promise<string> {
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "session", "-c", id]);
}

public async deleteSession(id: string): Promise<string> {
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "session", "-d", id]);
}

public async submitSolution(filePath: string): Promise<string> {
return await this.executeCommandWithProgressEx("Submitting to LeetCode...", "node", [await this.getLeetCodeBinaryPath(), "submit", `"${filePath}"`]);
try {
return await this.executeCommandWithProgressEx("Submitting to LeetCode...", this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "submit", `"${filePath}"`]);
} catch (error) {
if (error.result) {
return error.result;
}
throw error;
}
}

public async testSolution(filePath: string, testString?: string): Promise<string> {
if (testString) {
return await this.executeCommandWithProgressEx("Submitting to LeetCode...", "node", [await this.getLeetCodeBinaryPath(), "test", `"${filePath}"`, "-t", `${testString}`]);
return await this.executeCommandWithProgressEx("Submitting to LeetCode...", this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "test", `"${filePath}"`, "-t", `${testString}`]);
}
return await this.executeCommandWithProgressEx("Submitting to LeetCode...", "node", [await this.getLeetCodeBinaryPath(), "test", `"${filePath}"`]);
return await this.executeCommandWithProgressEx("Submitting to LeetCode...", this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "test", `"${filePath}"`]);
}

public async switchEndpoint(endpoint: string): Promise<string> {
switch (endpoint) {
case Endpoint.LeetCodeCN:
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", "leetcode.cn"]);
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "plugin", "-e", "leetcode.cn"]);
case Endpoint.LeetCode:
default:
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-d", "leetcode.cn"]);
return await this.executeCommandEx(this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "plugin", "-d", "leetcode.cn"]);
}
}

public async toggleFavorite(node: IProblem, addToFavorite: boolean): Promise<void> {
const commandParams: string[] = [await this.getLeetCodeBinaryPath(), "star", node.id];
if (!addToFavorite) {
commandParams.push("-d");
}
await this.executeCommandWithProgressEx("Updating the favorite list...", "node", commandParams);
}

public async getCompaniesAndTags(): Promise<{ companies: { [key: string]: string[] }, tags: { [key: string]: string[] } }> {
// preprocess the plugin source
const companiesTagsPath: string = path.join(await leetCodeExecutor.getLeetCodeRootPath(), "lib", "plugins", "company.js");
const companiesTagsPath: string = path.join(this.leetCodeRootPath, "lib", "plugins", "company.js");
const companiesTagsSrc: string = (await fse.readFile(companiesTagsPath, "utf8")).replace(
"module.exports = plugin",
"module.exports = { COMPONIES, TAGS }",
@@ -131,6 +209,19 @@ class LeetCodeExecutor {
return { companies: COMPONIES, tags: TAGS };
}

public get node(): string {
return this.nodeExecutable;
}

public dispose(): void {
this.configurationChangeListener.dispose();
}

private getNodePath(): string {
const extensionConfig: WorkspaceConfiguration = workspace.getConfiguration("leetcode", null);
return extensionConfig.get<string>("nodePath", "node" /* default value */);
}

private async executeCommandEx(command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise<string> {
if (wsl.useWsl()) {
return await executeCommand("wsl", [command].concat(args), options);
@@ -144,6 +235,14 @@ class LeetCodeExecutor {
}
return await executeCommandWithProgress(message, command, args, options);
}

private async removeOldCache(): Promise<void> {
const oldPath: string = path.join(os.homedir(), ".lc");
if (await fse.pathExists(oldPath)) {
await fse.remove(oldPath);
}
}

}

export const leetCodeExecutor: LeetCodeExecutor = new LeetCodeExecutor();
199 changes: 140 additions & 59 deletions src/leetCodeManager.ts
Original file line number Diff line number Diff line change
@@ -6,96 +6,120 @@ import { EventEmitter } from "events";
import * as vscode from "vscode";
import { leetCodeChannel } from "./leetCodeChannel";
import { leetCodeExecutor } from "./leetCodeExecutor";
import { UserStatus } from "./shared";
import { Endpoint, IQuickItemEx, loginArgsMapping, urls, urlsCn, UserStatus } from "./shared";
import { createEnvOption } from "./utils/cpUtils";
import { DialogType, promptForOpenOutputChannel } from "./utils/uiUtils";
import { DialogType, openUrl, promptForOpenOutputChannel } from "./utils/uiUtils";
import * as wsl from "./utils/wslUtils";
import { getLeetCodeEndpoint } from "./commands/plugin";
import { globalState } from "./globalState";
import { queryUserData } from "./request/query-user-data";
import { parseQuery } from "./utils/toolUtils";

class LeetCodeManager extends EventEmitter {
private currentUser: string | undefined;
private userStatus: UserStatus;
private readonly successRegex: RegExp = /(?:.*)Successfully .*login as (.*)/i;
private readonly failRegex: RegExp = /.*\[ERROR\].*/i;

constructor() {
super();
this.currentUser = undefined;
this.userStatus = UserStatus.SignedOut;
this.handleUriSignIn = this.handleUriSignIn.bind(this);
}

public async getLoginStatus(): Promise<void> {
try {
const result: string = await leetCodeExecutor.getUserInfo();
this.currentUser = result.slice("You are now login as".length).trim();
this.currentUser = this.tryParseUserName(result);
this.userStatus = UserStatus.SignedIn;
} catch (error) {
this.currentUser = undefined;
this.userStatus = UserStatus.SignedOut;
globalState.removeAll();
} finally {
this.emit("statusChanged");
}
}

public async signIn(): Promise<void> {
private async updateUserStatusWithCookie(cookie: string): Promise<void> {
globalState.setCookie(cookie);
const data = await queryUserData();
globalState.setUserStatus(data);
await this.setCookieToCli(cookie, data.username);
if (data.username) {
vscode.window.showInformationMessage(`Successfully ${data.username}.`);
this.currentUser = data.username;
this.userStatus = UserStatus.SignedIn;
this.emit("statusChanged");
}
}

public async handleUriSignIn(uri: vscode.Uri): Promise<void> {
try {
const userName: string | undefined = await new Promise(async (resolve: (res: string | undefined) => void, reject: (e: Error) => void): Promise<void> => {
let result: string = "";

const leetCodeBinaryPath: string = await leetCodeExecutor.getLeetCodeBinaryPath();

const childProc: cp.ChildProcess = wsl.useWsl()
? cp.spawn("wsl", ["node", leetCodeBinaryPath, "user", "-l"], { shell: true })
: cp.spawn("node", [leetCodeBinaryPath, "user", "-l"], {
shell: true,
env: createEnvOption(),
});

childProc.stdout.on("data", (data: string | Buffer) => {
data = data.toString();
result = result.concat(data);
leetCodeChannel.append(data);
});

childProc.stderr.on("data", (data: string | Buffer) => leetCodeChannel.append(data.toString()));

childProc.on("error", reject);
const name: string | undefined = await vscode.window.showInputBox({
prompt: "Enter user name.",
validateInput: (s: string): string | undefined => s && s.trim() ? undefined : "User name must not be empty",
});
if (!name) {
childProc.kill();
return resolve(undefined);
}
childProc.stdin.write(`${name}\n`);
const pwd: string | undefined = await vscode.window.showInputBox({
prompt: "Enter password.",
password: true,
validateInput: (s: string): string | undefined => s ? undefined : "Password must not be empty",
});
if (!pwd) {
childProc.kill();
return resolve(undefined);
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress: vscode.Progress<{}>) => {
progress.report({ message: "Fetching user data..." });
const queryParams = parseQuery(uri.query);
const cookie = queryParams["cookie"];
if (!cookie) {
promptForOpenOutputChannel(`Failed to get cookie. Please log in again`, DialogType.error);
return;
}
childProc.stdin.write(`${pwd}\n`);
childProc.stdin.end();
childProc.on("close", () => {
const match: RegExpMatchArray | null = result.match(/(?:.*) Successfully login as (.*)/i);
if (match && match[1]) {
resolve(match[1]);
} else {
reject(new Error("Failed to sign in."));
}
});

await this.updateUserStatusWithCookie(cookie)

});
if (userName) {
vscode.window.showInformationMessage("Successfully signed in.");
this.currentUser = userName;
this.userStatus = UserStatus.SignedIn;
this.emit("statusChanged");
}
} catch (error) {
promptForOpenOutputChannel("Failed to sign in. Please open the output channel for details", DialogType.error);
promptForOpenOutputChannel(`Failed to log in. Please open the output channel for details`, DialogType.error);
}
}

public async handleInputCookieSignIn(): Promise<void> {
const cookie: string | undefined = await vscode.window.showInputBox({
prompt: 'Enter LeetCode Cookie',
password: true,
ignoreFocusOut: true,
validateInput: (s: string): string | undefined =>
s ? undefined : 'Cookie must not be empty',
})

await this.updateUserStatusWithCookie(cookie || '')
}

public async signIn(): Promise<void> {
const picks: Array<IQuickItemEx<string>> = []
picks.push(
{
label: 'Web Authorization',
detail: 'Open browser to authorize login on the website',
value: 'WebAuth',
description: '[Recommended]'
},
{
label: 'LeetCode Cookie',
detail: 'Use LeetCode cookie copied from browser to login',
value: 'Cookie',
}
)

const choice: IQuickItemEx<string> | undefined = await vscode.window.showQuickPick(picks)
if (!choice) {
return
}
const loginMethod: string = choice.value

if (loginMethod === 'WebAuth') {
openUrl(this.getAuthLoginUrl())
return
}

try {
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Fetching user data..." }, async () => {
await this.handleInputCookieSignIn()
});
} catch (error) {
promptForOpenOutputChannel(`Failed to log in. Please open the output channel for details`, DialogType.error);
}
}

public async signOut(): Promise<void> {
@@ -104,6 +128,7 @@ class LeetCodeManager extends EventEmitter {
vscode.window.showInformationMessage("Successfully signed out.");
this.currentUser = undefined;
this.userStatus = UserStatus.SignedOut;
globalState.removeAll();
this.emit("statusChanged");
} catch (error) {
// swallow the error when sign out.
@@ -117,6 +142,62 @@ class LeetCodeManager extends EventEmitter {
public getUser(): string | undefined {
return this.currentUser;
}

private tryParseUserName(output: string): string {
const reg: RegExp = /^\s*.\s*(.+?)\s*https:\/\/leetcode/m;
const match: RegExpMatchArray | null = output.match(reg);
if (match && match.length === 2) {
return match[1].trim();
}

return "Unknown";
}

public getAuthLoginUrl(): string {
switch (getLeetCodeEndpoint()) {
case Endpoint.LeetCodeCN:
return urlsCn.authLoginUrl;
case Endpoint.LeetCode:
default:
return urls.authLoginUrl;
}
}

public setCookieToCli(cookie: string, name: string): Promise<void> {
return new Promise(async (resolve: (res: void) => void, reject: (e: Error) => void) => {
const leetCodeBinaryPath: string = await leetCodeExecutor.getLeetCodeBinaryPath();

const childProc: cp.ChildProcess = wsl.useWsl()
? cp.spawn("wsl", [leetCodeExecutor.node, leetCodeBinaryPath, "user", loginArgsMapping.get("Cookie") ?? ""], {
shell: true,
})
: cp.spawn(leetCodeExecutor.node, [leetCodeBinaryPath, "user", loginArgsMapping.get("Cookie") ?? ""], {
shell: true,
env: createEnvOption(),
});

childProc.stdout?.on("data", async (data: string | Buffer) => {
data = data.toString();
leetCodeChannel.append(data);
const successMatch: RegExpMatchArray | null = data.match(this.successRegex);
if (successMatch && successMatch[1]) {
childProc.stdin?.end();
return resolve();
} else if (data.match(this.failRegex)) {
childProc.stdin?.end();
return reject(new Error("Faile to login"));
} else if (data.match(/login: /)) {
childProc.stdin?.write(`${name}\n`);
} else if (data.match(/cookie: /)) {
childProc.stdin?.write(`${cookie}\n`);
}
});

childProc.stderr?.on("data", (data: string | Buffer) => leetCodeChannel.append(data.toString()));

childProc.on("error", reject);
});
}
}

export const leetCodeManager: LeetCodeManager = new LeetCodeManager();
52 changes: 0 additions & 52 deletions src/leetCodeResultProvider.ts

This file was deleted.

25 changes: 25 additions & 0 deletions src/request/query-user-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { UserDataType } from "../globalState";
import { getUrl } from "../shared";
import { LcAxios } from "../utils/httpUtils";

const graphqlStr = `
query globalData {
userStatus {
isPremium
isVerified
username
avatar
isSignedIn
}
}
`;

export const queryUserData = async (): Promise<UserDataType> => {
return LcAxios(getUrl("userGraphql"), {
method: "POST",
data: {
query: graphqlStr,
variables: {},
},
}).then((res) => res.data.data.userStatus);
};
65 changes: 65 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
@@ -12,6 +12,13 @@ export enum UserStatus {
SignedOut = 2,
}

export const loginArgsMapping: Map<string, string> = new Map([
["LeetCode", "-l"],
["Cookie", "-c"],
["GitHub", "-g"],
["LinkedIn", "-i"],
]);

export const languages: string[] = [
"bash",
"c",
@@ -29,6 +36,7 @@ export const languages: string[] = [
"rust",
"scala",
"swift",
"typescript",
];

export const langExt: Map<string, string> = new Map([
@@ -48,12 +56,14 @@ export const langExt: Map<string, string> = new Map([
["rust", "rs"],
["scala", "scala"],
["swift", "swift"],
["typescript", "ts"],
]);

export enum ProblemState {
AC = 1,
NotAC = 2,
Unknown = 3,
Locked = 4,
}

export enum Endpoint {
@@ -86,8 +96,63 @@ export const defaultProblem: IProblem = {
};

export enum Category {
All = "All",
Difficulty = "Difficulty",
Tag = "Tag",
Company = "Company",
Favorite = "Favorite",
}

export const supportedPlugins: string[] = ["company", "solution.discuss", "leetcode.cn"];

export enum DescriptionConfiguration {
InWebView = "In Webview",
InFileComment = "In File Comment",
Both = "Both",
None = "None",
}

export const leetcodeHasInited: string = "leetcode.hasInited";

export enum SortingStrategy {
None = "None",
AcceptanceRateAsc = "Acceptance Rate (Ascending)",
AcceptanceRateDesc = "Acceptance Rate (Descending)",
FrequencyAsc = "Frequency (Ascending)",
FrequencyDesc = "Frequency (Descending)",
}

export const PREMIUM_URL_CN = "https://leetcode.cn/premium-payment/?source=vscode";
export const PREMIUM_URL_GLOBAL = "https://leetcode.com/subscribe/?ref=lp_pl&source=vscode";

const protocol = vscode.env.appName.includes('Insiders') ? "vscode-insiders" : "vscode"

export const urls = {
// base urls
base: "https://leetcode.com",
graphql: "https://leetcode.com/graphql",
userGraphql: "https://leetcode.com/graphql",
login: "https://leetcode.com/accounts/login/",
authLoginUrl: `https://leetcode.com/authorize-login/${protocol}/?path=leetcode.vscode-leetcode`,
};

export const urlsCn = {
// base urls
base: "https://leetcode.cn",
graphql: "https://leetcode.cn/graphql",
userGraphql: "https://leetcode.cn/graphql/",
login: "https://leetcode.cn/accounts/login/",
authLoginUrl: `https://leetcode.cn/authorize-login/${protocol}/?path=leetcode.vscode-leetcode`,
};

export const getUrl = (key: string) => {
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
const point = leetCodeConfig.get<string>("endpoint", Endpoint.LeetCode);
switch (point) {
case Endpoint.LeetCodeCN:
return urlsCn[key];
case Endpoint.LeetCode:
default:
return urls[key];
}
};
Original file line number Diff line number Diff line change
@@ -2,32 +2,37 @@
// Licensed under the MIT license.

import * as vscode from "vscode";
import { UserStatus } from "./shared";
import { UserStatus } from "../shared";

class LeetCodeStatusBarItem implements vscode.Disposable {
export class LeetCodeStatusBarItem implements vscode.Disposable {
private readonly statusBarItem: vscode.StatusBarItem;

constructor() {
this.statusBarItem = vscode.window.createStatusBarItem();
this.statusBarItem.command = "leetcode.selectSessions";
this.statusBarItem.command = "leetcode.manageSessions";
}

public updateStatusBar(status: UserStatus, user?: string): void {
switch (status) {
case UserStatus.SignedIn:
this.statusBarItem.text = `LeetCode: ${user}`;
this.statusBarItem.show();
break;
case UserStatus.SignedOut:
default:
this.statusBarItem.hide();
this.statusBarItem.text = "";
break;
}
}

public show(): void {
this.statusBarItem.show();
}

public hide(): void {
this.statusBarItem.hide();
}

public dispose(): void {
this.statusBarItem.dispose();
}
}

export const leetCodeStatusBarItem: LeetCodeStatusBarItem = new LeetCodeStatusBarItem();
46 changes: 46 additions & 0 deletions src/statusbar/leetCodeStatusBarController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { ConfigurationChangeEvent, Disposable, workspace, WorkspaceConfiguration } from "vscode";
import { UserStatus } from "../shared";
import { LeetCodeStatusBarItem } from "./LeetCodeStatusBarItem";

class LeetCodeStatusBarController implements Disposable {
private statusBar: LeetCodeStatusBarItem;
private configurationChangeListener: Disposable;

constructor() {
this.statusBar = new LeetCodeStatusBarItem();
this.setStatusBarVisibility();

this.configurationChangeListener = workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
if (event.affectsConfiguration("leetcode.enableStatusBar")) {
this.setStatusBarVisibility();
}
}, this);
}

public updateStatusBar(status: UserStatus, user?: string): void {
this.statusBar.updateStatusBar(status, user);
}

public dispose(): void {
this.statusBar.dispose();
this.configurationChangeListener.dispose();
}

private setStatusBarVisibility(): void {
if (this.isStatusBarEnabled()) {
this.statusBar.show();
} else {
this.statusBar.hide();
}
}

private isStatusBarEnabled(): boolean {
const configuration: WorkspaceConfiguration = workspace.getConfiguration();
return configuration.get<boolean>("leetcode.enableStatusBar", true);
}
}

export const leetCodeStatusBarController: LeetCodeStatusBarController = new LeetCodeStatusBarController();
17 changes: 13 additions & 4 deletions src/utils/cpUtils.ts
Original file line number Diff line number Diff line change
@@ -5,24 +5,33 @@ import * as cp from "child_process";
import * as vscode from "vscode";
import { leetCodeChannel } from "../leetCodeChannel";

interface IExecError extends Error {
result?: string;
}

export async function executeCommand(command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise<string> {
return new Promise((resolve: (res: string) => void, reject: (e: Error) => void): void => {
let result: string = "";

const childProc: cp.ChildProcess = cp.spawn(command, args, { ...options, env: createEnvOption() });

childProc.stdout.on("data", (data: string | Buffer) => {
childProc.stdout?.on("data", (data: string | Buffer) => {
data = data.toString();
result = result.concat(data);
leetCodeChannel.append(data);
});

childProc.stderr.on("data", (data: string | Buffer) => leetCodeChannel.append(data.toString()));
childProc.stderr?.on("data", (data: string | Buffer) => leetCodeChannel.append(data.toString()));

childProc.on("error", reject);

childProc.on("close", (code: number) => {
if (code !== 0 || result.indexOf("ERROR") > -1) {
reject(new Error(`Command "${command} ${args.toString()}" failed with exit code "${code}".`));
const error: IExecError = new Error(`Command "${command} ${args.toString()}" failed with exit code "${code}".`);
if (result) {
error.result = result; // leetcode-cli may print useful content by exit with error code
}
reject(error);
} else {
resolve(result);
}
@@ -33,7 +42,7 @@ export async function executeCommand(command: string, args: string[], options: c
export async function executeCommandWithProgress(message: string, command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise<string> {
let result: string = "";
await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (p: vscode.Progress<{}>) => {
return new Promise(async (resolve: () => void, reject: (e: Error) => void): Promise<void> => {
return new Promise<void>(async (resolve: () => void, reject: (e: Error) => void): Promise<void> => {
p.report({ message });
try {
result = await executeCommand(command, args, options);
28 changes: 28 additions & 0 deletions src/utils/httpUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import axios, { AxiosRequestConfig, AxiosPromise } from "axios";
import { omit } from "lodash";
import { globalState } from "../globalState";
import { DialogType, promptForOpenOutputChannel } from "./uiUtils";

const referer = "vscode-lc-extension";

export function LcAxios<T = any>(path: string, settings?: AxiosRequestConfig): AxiosPromise<T> {
const cookie = globalState.getCookie();
if (!cookie) {
promptForOpenOutputChannel(
`Failed to obtain the cookie. Please log in again.`,
DialogType.error
);
return Promise.reject("Failed to obtain the cookie.");
}
return axios(path, {
headers: {
referer,
"content-type": "application/json",
cookie,
...(settings && settings.headers),
},
xsrfCookieName: "csrftoken",
xsrfHeaderName: "X-CSRFToken",
...(settings && omit(settings, "headers")),
});
}
24 changes: 22 additions & 2 deletions src/utils/problemUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import kebabCase = require("lodash.kebabcase");
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import * as fse from "fs-extra";
import * as _ from "lodash";
import * as path from "path";
import { IProblem, langExt } from "../shared";

export function genFileExt(language: string): string {
@@ -10,7 +15,22 @@ export function genFileExt(language: string): string {
}

export function genFileName(node: IProblem, language: string): string {
const slug: string = kebabCase(node.name);
const slug: string = _.kebabCase(node.name);
const ext: string = genFileExt(language);
return `${node.id}.${slug}.${ext}`;
}

export async function getNodeIdFromFile(fsPath: string): Promise<string> {
const fileContent: string = await fse.readFile(fsPath, "utf8");
let id: string = "";
const matchResults: RegExpMatchArray | null = fileContent.match(/@lc.+id=(.+?) /);
if (matchResults && matchResults.length === 2) {
id = matchResults[1];
}
// Try to get id from file name if getting from comments failed
if (!id) {
id = path.basename(fsPath).split(".")[0];
}

return id;
}
68 changes: 68 additions & 0 deletions src/utils/settingUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { workspace, WorkspaceConfiguration } from "vscode";
import { DescriptionConfiguration } from "../shared";

export function getWorkspaceConfiguration(): WorkspaceConfiguration {
return workspace.getConfiguration("leetcode");
}

export function shouldHideSolvedProblem(): boolean {
return getWorkspaceConfiguration().get<boolean>("hideSolved", false);
}

export function getWorkspaceFolder(): string {
return getWorkspaceConfiguration().get<string>("workspaceFolder", "");
}

export function getEditorShortcuts(): string[] {
return getWorkspaceConfiguration().get<string[]>("editor.shortcuts", ["submit", "test"]);
}

export function hasStarShortcut(): boolean {
const shortcuts: string[] = getWorkspaceConfiguration().get<string[]>("editor.shortcuts", ["submit", "test"]);
return shortcuts.indexOf("star") >= 0;
}

export function shouldUseEndpointTranslation(): boolean {
return getWorkspaceConfiguration().get<boolean>("useEndpointTranslation", true);
}

export function getDescriptionConfiguration(): IDescriptionConfiguration {
const setting: string = getWorkspaceConfiguration().get<string>("showDescription", DescriptionConfiguration.InWebView);
const config: IDescriptionConfiguration = {
showInComment: false,
showInWebview: true,
};
switch (setting) {
case DescriptionConfiguration.Both:
config.showInComment = true;
config.showInWebview = true;
break;
case DescriptionConfiguration.None:
config.showInComment = false;
config.showInWebview = false;
break;
case DescriptionConfiguration.InFileComment:
config.showInComment = true;
config.showInWebview = false;
break;
case DescriptionConfiguration.InWebView:
config.showInComment = false;
config.showInWebview = true;
break;
}

// To be compatible with the deprecated setting:
if (getWorkspaceConfiguration().get<boolean>("showCommentDescription")) {
config.showInComment = true;
}

return config;
}

export interface IDescriptionConfiguration {
showInComment: boolean;
showInWebview: boolean;
}
26 changes: 26 additions & 0 deletions src/utils/toolUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export function parseQuery(query: string): { [key: string]: string } {
const queryObject: { [key: string]: string } = {};

if (!query) {
return queryObject;
}

const keyValuePairs = query.split("&");
keyValuePairs.forEach((pair) => {
const firstEqualsIndex = pair.indexOf("=");
if (firstEqualsIndex !== -1) {
const key = pair.substring(0, firstEqualsIndex);
const value = pair.substring(firstEqualsIndex + 1);
queryObject[decodeURIComponent(key)] = decodeURIComponent(value);
} else {
// If no equals sign is found, treat the whole string as key with empty value
queryObject[decodeURIComponent(pair)] = "";
}
});

return queryObject;
}
130 changes: 130 additions & 0 deletions src/utils/trackingUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as vscode from "vscode";
import axios from "axios";
import { getLeetCodeEndpoint } from "../commands/plugin";
import { Endpoint } from "../shared";
import { leetCodeManager } from "../leetCodeManager";

const getTimeZone = (): string => {
const endPoint: string = getLeetCodeEndpoint();
if (endPoint === Endpoint.LeetCodeCN) {
return "Asia/Shanghai";
} else {
return "UTC";
}
};

interface IReportData {
event_key: string;
type?: "click" | "expose" | string;
anonymous_id?: string;
tid?: number;
ename?: string; // event name
href?: string;
referer?: string;
extra?: string;
target?: string;
}

interface ITrackData {
reportCache: IReportData[];
isSubmit: boolean;
report: (reportItems: IReportData | IReportData[]) => void;
submitReport: (useSendBeason: boolean) => Promise<void>;
reportUrl: string;
}

const testReportUrl = "https://analysis.lingkou.xyz/i/event";
const prodReportUrl = "https://analysis.leetcode.cn/i/event";

function getReportUrl() {
if (process.env.NODE_ENV === "production") {
return prodReportUrl;
} else {
return testReportUrl;
}
}

const _charStr = "abacdefghjklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+=";

function RandomIndex(min: number, max: number, i: number) {
let index = Math.floor(Math.random() * (max - min + 1) + min);
const numStart = _charStr.length - 10;
if (i === 0 && index >= numStart) {
index = RandomIndex(min, max, i);
}
return index;
}

function getRandomString(len: number) {
const min = 0;
const max = _charStr.length - 1;
let _str = "";
len = len || 15;
for (let i = 0, index; i < len; i++) {
index = RandomIndex(min, max, i);
_str += _charStr[index];
}
return _str;
}

function getAllowReportDataConfig() {
const leetCodeConfig = vscode.workspace.getConfiguration("leetcode");
const allowReportData = !!leetCodeConfig.get<boolean>("allowReportData");
return allowReportData;
}

class TrackData implements ITrackData {
public reportCache: IReportData[] = [];

public isSubmit: boolean = false;

public reportUrl: string = getReportUrl();

private sendTimer: NodeJS.Timeout | undefined;

public report = (reportItems: IReportData | IReportData[]) => {
if (!getAllowReportDataConfig()) return;

this.sendTimer && clearTimeout(this.sendTimer);

if (!Array.isArray(reportItems)) {
reportItems = [reportItems];
}
const randomId = getRandomString(60);
reportItems.forEach((item) => {
this.reportCache.push({
...item,
referer: "vscode",
target: leetCodeManager.getUser() ?? "",
anonymous_id: item.anonymous_id ?? (randomId as string),
});
});
this.sendTimer = setTimeout(this.submitReport, 800);
};

public submitReport = async () => {
if (!getAllowReportDataConfig()) return;
const dataList = JSON.stringify(this.reportCache);

if (!this.reportCache.length || this.isSubmit) {
return;
}
this.reportCache = [];
try {
this.isSubmit = true;
axios.defaults.withCredentials = true;
await axios.post(this.reportUrl, `dataList=${encodeURIComponent(dataList)}`, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"x-timezone": getTimeZone(),
},
});
} catch (e) {
this.reportCache = this.reportCache.concat(JSON.parse(dataList));
} finally {
this.isSubmit = false;
}
};
}

export default new TrackData();
52 changes: 49 additions & 3 deletions src/utils/uiUtils.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
import * as vscode from "vscode";
import { getLeetCodeEndpoint } from "../commands/plugin";
import { leetCodeChannel } from "../leetCodeChannel";
import { getWorkspaceConfiguration } from "./settingUtils";

export namespace DialogOptions {
export const open: vscode.MessageItem = { title: "Open" };
@@ -47,7 +48,7 @@ export async function promptForSignIn(): Promise<void> {
break;
case DialogOptions.singUp:
if (getLeetCodeEndpoint()) {
openUrl("https://leetcode-cn.com");
openUrl("https://leetcode.cn");
} else {
openUrl("https://leetcode.com");
}
@@ -57,8 +58,30 @@ export async function promptForSignIn(): Promise<void> {
}
}

export async function showFileSelectDialog(): Promise<vscode.Uri[] | undefined> {
const defaultUri: vscode.Uri | undefined = vscode.workspace.rootPath ? vscode.Uri.file(vscode.workspace.rootPath) : undefined;
export async function promptHintMessage(config: string, message: string, choiceConfirm: string, onConfirm: () => Promise<any>): Promise<void> {
if (getWorkspaceConfiguration().get<boolean>(config)) {
const choiceNoShowAgain: string = "Don't show again";
const choice: string | undefined = await vscode.window.showInformationMessage(
message, choiceConfirm, choiceNoShowAgain,
);
if (choice === choiceConfirm) {
await onConfirm();
} else if (choice === choiceNoShowAgain) {
await getWorkspaceConfiguration().update(config, false, true /* UserSetting */);
}
}
}

export async function openSettingsEditor(query?: string): Promise<void> {
await vscode.commands.executeCommand("workbench.action.openSettings", query);
}

export async function openKeybindingsEditor(query?: string): Promise<void> {
await vscode.commands.executeCommand("workbench.action.openGlobalKeybindings", query);
}

export async function showFileSelectDialog(fsPath?: string): Promise<vscode.Uri[] | undefined> {
const defaultUri: vscode.Uri | undefined = getBelongingWorkspaceFolderUri(fsPath);
const options: vscode.OpenDialogOptions = {
defaultUri,
canSelectFiles: true,
@@ -69,6 +92,29 @@ export async function showFileSelectDialog(): Promise<vscode.Uri[] | undefined>
return await vscode.window.showOpenDialog(options);
}

function getBelongingWorkspaceFolderUri(fsPath: string | undefined): vscode.Uri | undefined {
let defaultUri: vscode.Uri | undefined;
if (fsPath) {
const workspaceFolder: vscode.WorkspaceFolder | undefined = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(fsPath));
if (workspaceFolder) {
defaultUri = workspaceFolder.uri;
}
}
return defaultUri;
}

export async function showDirectorySelectDialog(fsPath?: string): Promise<vscode.Uri[] | undefined> {
const defaultUri: vscode.Uri | undefined = getBelongingWorkspaceFolderUri(fsPath);
const options: vscode.OpenDialogOptions = {
defaultUri,
canSelectFiles: false,
canSelectFolders: true,
canSelectMany: false,
openLabel: "Select",
};
return await vscode.window.showOpenDialog(options);
}

export async function openUrl(url: string): Promise<void> {
vscode.commands.executeCommand("vscode.open", vscode.Uri.parse(url));
}
107 changes: 95 additions & 12 deletions src/utils/workspaceUtils.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,62 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import * as fse from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as vscode from "vscode";
import { IQuickItemEx } from "../shared";
import { getWorkspaceConfiguration, getWorkspaceFolder } from "./settingUtils";
import { showDirectorySelectDialog } from "./uiUtils";
import * as wsl from "./wslUtils";

export async function selectWorkspaceFolder(): Promise<string> {
let folder: vscode.WorkspaceFolder | undefined;
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
if (vscode.workspace.workspaceFolders.length > 1) {
folder = await vscode.window.showWorkspaceFolderPick({
placeHolder: "Select the working directory you wish to use",
});
} else {
folder = vscode.workspace.workspaceFolders[0];
let workspaceFolderSetting: string = getWorkspaceFolder();
if (workspaceFolderSetting.trim() === "") {
workspaceFolderSetting = await determineLeetCodeFolder();
if (workspaceFolderSetting === "") {
// User cancelled
return workspaceFolderSetting;
}
}
let needAsk: boolean = true;
await fse.ensureDir(workspaceFolderSetting);
for (const folder of vscode.workspace.workspaceFolders || []) {
if (isSubFolder(folder.uri.fsPath, workspaceFolderSetting)) {
needAsk = false;
}
}

const workFolder: string = folder ? folder.uri.fsPath : path.join(os.homedir(), ".leetcode");
if (needAsk) {
const choice: string | undefined = await vscode.window.showQuickPick(
[
OpenOption.justOpenFile,
OpenOption.openInCurrentWindow,
OpenOption.openInNewWindow,
OpenOption.addToWorkspace,
],
{ placeHolder: "The LeetCode workspace folder is not opened in VS Code, would you like to open it?" },
);

return wsl.useWsl() ? wsl.toWslPath(workFolder) : workFolder;
// Todo: generate file first
switch (choice) {
case OpenOption.justOpenFile:
return workspaceFolderSetting;
case OpenOption.openInCurrentWindow:
await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(workspaceFolderSetting), false);
return "";
case OpenOption.openInNewWindow:
await vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(workspaceFolderSetting), true);
return "";
case OpenOption.addToWorkspace:
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length ?? 0, 0, { uri: vscode.Uri.file(workspaceFolderSetting) });
break;
default:
return "";
}
}

return wsl.useWsl() ? wsl.toWslPath(workspaceFolderSetting) : workspaceFolderSetting;
}

export async function getActiveFilePath(uri?: vscode.Uri): Promise<string | undefined> {
@@ -41,6 +77,53 @@ export async function getActiveFilePath(uri?: vscode.Uri): Promise<string | unde
return wsl.useWsl() ? wsl.toWslPath(textEditor.document.uri.fsPath) : textEditor.document.uri.fsPath;
}

export function getWorkspaceConfiguration(): vscode.WorkspaceConfiguration {
return vscode.workspace.getConfiguration("leetcode");
function isSubFolder(from: string, to: string): boolean {
const relative: string = path.relative(from, to);
if (relative === "") {
return true;
}
return !relative.startsWith("..") && !path.isAbsolute(relative);
}

async function determineLeetCodeFolder(): Promise<string> {
let result: string;
const picks: Array<IQuickItemEx<string>> = [];
picks.push(
{
label: `Default location`,
detail: `${path.join(os.homedir(), ".leetcode")}`,
value: `${path.join(os.homedir(), ".leetcode")}`,
},
{
label: "$(file-directory) Browse...",
value: ":browse",
},
);
const choice: IQuickItemEx<string> | undefined = await vscode.window.showQuickPick(
picks,
{ placeHolder: "Select where you would like to save your LeetCode files" },
);
if (!choice) {
result = "";
} else if (choice.value === ":browse") {
const directory: vscode.Uri[] | undefined = await showDirectorySelectDialog();
if (!directory || directory.length < 1) {
result = "";
} else {
result = directory[0].fsPath;
}
} else {
result = choice.value;
}

getWorkspaceConfiguration().update("workspaceFolder", result, vscode.ConfigurationTarget.Global);

return result;
}

enum OpenOption {
justOpenFile = "Just open the problem file",
openInCurrentWindow = "Open in current window",
openInNewWindow = "Open in new window",
addToWorkspace = "Add to workspace",
}
8 changes: 6 additions & 2 deletions src/utils/wslUtils.ts
Original file line number Diff line number Diff line change
@@ -3,16 +3,20 @@

import * as vscode from "vscode";
import { executeCommand } from "./cpUtils";
import { isWindows } from "./osUtils";

export function useWsl(): boolean {
const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode");
return process.platform === "win32" && leetCodeConfig.get<boolean>("useWsl") === true;
return isWindows() && leetCodeConfig.get<boolean>("useWsl") === true;
}

export async function toWslPath(path: string): Promise<string> {
return (await executeCommand("wsl", ["wslpath", "-u", `"${path.replace(/\\/g, "/")}"`])).trim();
}

export async function toWinPath(path: string): Promise<string> {
return (await executeCommand("wsl", ["wslpath", "-w", `"${path}"`])).trim();
if (path.startsWith("\\mnt\\")) {
return (await executeCommand("wsl", ["wslpath", "-w", `"${path.replace(/\\/g, "/").substr(0, 6)}"`])).trim() + path.substr(7);
}
return (await executeCommand("wsl", ["wslpath", "-w", "/"])).trim() + path;
}
82 changes: 82 additions & 0 deletions src/webview/LeetCodeWebview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { commands, ConfigurationChangeEvent, Disposable, ViewColumn, WebviewPanel, window, workspace } from "vscode";
import { openSettingsEditor, promptHintMessage } from "../utils/uiUtils";
import { markdownEngine } from "./markdownEngine";

export abstract class LeetCodeWebview implements Disposable {

protected readonly viewType: string = "leetcode.webview";
protected panel: WebviewPanel | undefined;
private listeners: Disposable[] = [];

public dispose(): void {
if (this.panel) {
this.panel.dispose();
}
}

protected showWebviewInternal(): void {
const { title, viewColumn, preserveFocus } = this.getWebviewOption();
if (!this.panel) {
this.panel = window.createWebviewPanel(this.viewType, title, { viewColumn, preserveFocus }, {
enableScripts: true,
enableCommandUris: true,
enableFindWidget: true,
retainContextWhenHidden: true,
localResourceRoots: markdownEngine.localResourceRoots,
});
this.panel.onDidDispose(this.onDidDisposeWebview, this, this.listeners);
this.panel.webview.onDidReceiveMessage(this.onDidReceiveMessage, this, this.listeners);
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.listeners);
} else {
this.panel.title = title;
if (viewColumn === ViewColumn.Two) {
// Make sure second group exists. See vscode#71608 issue
commands.executeCommand("workbench.action.focusSecondEditorGroup").then(() => {
this.panel!.reveal(viewColumn, preserveFocus);
});
} else {
this.panel.reveal(viewColumn, preserveFocus);
}
}
this.panel.webview.html = this.getWebviewContent();
this.showMarkdownConfigHint();
}

protected onDidDisposeWebview(): void {
this.panel = undefined;
for (const listener of this.listeners) {
listener.dispose();
}
this.listeners = [];
}

protected async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise<void> {
if (this.panel && event.affectsConfiguration("markdown")) {
this.panel.webview.html = this.getWebviewContent();
}
}

protected async onDidReceiveMessage(_message: any): Promise<void> { /* no special rule */ }

protected abstract getWebviewOption(): ILeetCodeWebviewOption;

protected abstract getWebviewContent(): string;

private async showMarkdownConfigHint(): Promise<void> {
await promptHintMessage(
"hint.configWebviewMarkdown",
'You can change the webview appearance ("fontSize", "lineWidth" & "fontFamily") in "markdown.preview" configuration.',
"Open settings",
(): Promise<any> => openSettingsEditor("markdown.preview"),
);
}
}

export interface ILeetCodeWebviewOption {
title: string;
viewColumn: ViewColumn;
preserveFocus?: boolean;
}
206 changes: 206 additions & 0 deletions src/webview/leetCodePreviewProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { commands, ViewColumn } from "vscode";
import { getLeetCodeEndpoint } from "../commands/plugin";
import { Endpoint, IProblem } from "../shared";
import { ILeetCodeWebviewOption, LeetCodeWebview } from "./LeetCodeWebview";
import { markdownEngine } from "./markdownEngine";

class LeetCodePreviewProvider extends LeetCodeWebview {
protected readonly viewType: string = "leetcode.preview";
private node: IProblem;
private description: IDescription;
private sideMode: boolean = false;

public isSideMode(): boolean {
return this.sideMode;
}

public show(descString: string, node: IProblem, isSideMode: boolean = false): void {
this.description = this.parseDescription(descString, node);
this.node = node;
this.sideMode = isSideMode;
this.showWebviewInternal();
}

protected getWebviewOption(): ILeetCodeWebviewOption {
if (!this.sideMode) {
return {
title: `${this.node.name}: Preview`,
viewColumn: ViewColumn.One,
};
} else {
return {
title: "Description",
viewColumn: ViewColumn.Two,
preserveFocus: true,
};
}
}

protected getWebviewContent(): string {
const button: { element: string; script: string; style: string } = {
element: `<button id="solve">Code Now</button>`,
script: `const button = document.getElementById('solve');
button.onclick = () => vscode.postMessage({
command: 'ShowProblem',
});`,
style: `<style>
#solve {
position: fixed;
bottom: 1rem;
right: 1rem;
border: 0;
margin: 1rem 0;
padding: 0.2rem 1rem;
color: white;
background-color: var(--vscode-button-background);
}
#solve:hover {
background-color: var(--vscode-button-hoverBackground);
}
#solve:active {
border: 0;
}
</style>`,
};
const { title, url, category, difficulty, likes, dislikes, body } = this.description;
const head: string = markdownEngine.render(`# [${title}](${url})`);
const info: string = markdownEngine.render(
[
`| Category | Difficulty | Likes | Dislikes |`,
`| :------: | :--------: | :---: | :------: |`,
`| ${category} | ${difficulty} | ${likes} | ${dislikes} |`,
].join("\n")
);
const tags: string = [
`<details>`,
`<summary><strong>Tags</strong></summary>`,
markdownEngine.render(this.description.tags.map((t: string) => `[\`${t}\`](${this.getTagLink(t)})`).join(" | ")),
`</details>`,
].join("\n");
const companies: string = [
`<details>`,
`<summary><strong>Companies</strong></summary>`,
markdownEngine.render(this.description.companies.map((c: string) => `\`${c}\``).join(" | ")),
`</details>`,
].join("\n");
const links: string = markdownEngine.render(`[Submissions](${this.getSubmissionsLink(url)}) | [Solution](${this.getSolutionsLink(url)})`);
return `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https:; script-src vscode-resource: 'unsafe-inline'; style-src vscode-resource: 'unsafe-inline';"/>
${markdownEngine.getStyles()}
${!this.sideMode ? button.style : ""}
<style>
code { white-space: pre-wrap; }
</style>
</head>
<body>
${head}
${info}
${tags}
${companies}
${body}
<hr />
${links}
${!this.sideMode ? button.element : ""}
<script>
const vscode = acquireVsCodeApi();
${!this.sideMode ? button.script : ""}
</script>
</body>
</html>
`;
}

protected onDidDisposeWebview(): void {
super.onDidDisposeWebview();
this.sideMode = false;
}

protected async onDidReceiveMessage(message: IWebViewMessage): Promise<void> {
switch (message.command) {
case "ShowProblem": {
await commands.executeCommand("leetcode.showProblem", this.node);
break;
}
}
}

// private async hideSideBar(): Promise<void> {
// await commands.executeCommand("workbench.action.focusSideBar");
// await commands.executeCommand("workbench.action.toggleSidebarVisibility");
// }

private parseDescription(descString: string, problem: IProblem): IDescription {
const [
,
,
/* title */ url,
,
,
,
,
,
/* tags */ /* langs */ category,
difficulty,
likes,
dislikes,
,
,
,
,
/* accepted */ /* submissions */ /* testcase */ ...body
] = descString.split("\n");
return {
title: problem.name,
url,
tags: problem.tags,
companies: problem.companies,
category: category.slice(2),
difficulty: difficulty.slice(2),
likes: likes.split(": ")[1].trim(),
dislikes: dislikes.split(": ")[1].trim(),
body: body.join("\n").replace(/<pre>[\r\n]*([^]+?)[\r\n]*<\/pre>/g, "<pre><code>$1</code></pre>"),
};
}

private getTagLink(tag: string): string {
const endPoint: string = getLeetCodeEndpoint();
if (endPoint === Endpoint.LeetCodeCN) {
return `https://leetcode.cn/tag/${tag}?source=vscode`;
} else if (endPoint === Endpoint.LeetCode) {
return `https://leetcode.com/tag/${tag}?source=vscode`;
}

return "https://leetcode.com?source=vscode";
}

private getSolutionsLink(url: string): string {
return url.replace("/description/", "/solutions/") + "?source=vscode";
}
private getSubmissionsLink(url: string): string {
return url.replace("/description/", "/submissions/") + "?source=vscode";
}
}

interface IDescription {
title: string;
url: string;
tags: string[];
companies: string[];
category: string;
difficulty: string;
likes: string;
dislikes: string;
body: string;
}

interface IWebViewMessage {
command: string;
}

export const leetCodePreviewProvider: LeetCodePreviewProvider = new LeetCodePreviewProvider();
94 changes: 94 additions & 0 deletions src/webview/leetCodeSolutionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { ViewColumn } from "vscode";
import { leetCodePreviewProvider } from "./leetCodePreviewProvider";
import { ILeetCodeWebviewOption, LeetCodeWebview } from "./LeetCodeWebview";
import { markdownEngine } from "./markdownEngine";

class LeetCodeSolutionProvider extends LeetCodeWebview {

protected readonly viewType: string = "leetcode.solution";
private problemName: string;
private solution: Solution;

public show(solutionString: string): void {
this.solution = this.parseSolution(solutionString);
this.showWebviewInternal();
}

protected getWebviewOption(): ILeetCodeWebviewOption {
if (leetCodePreviewProvider.isSideMode()) {
return {
title: "Solution",
viewColumn: ViewColumn.Two,
preserveFocus: true,
};
} else {
return {
title: `Solution: ${this.problemName}`,
viewColumn: ViewColumn.One,
};
}
}

protected getWebviewContent(): string {
const styles: string = markdownEngine.getStyles();
const { title, url, lang, author, votes } = this.solution;
const head: string = markdownEngine.render(`# [${title}](${url})`);
const auth: string = `[${author}](https://leetcode.com/${author}/)`;
const info: string = markdownEngine.render([
`| Language | Author | Votes |`,
`| :------: | :------: | :------: |`,
`| ${lang} | ${auth} | ${votes} |`,
].join("\n"));
const body: string = markdownEngine.render(this.solution.body, {
lang: this.solution.lang,
host: "https://discuss.leetcode.com/",
});
return `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https:; script-src vscode-resource:; style-src vscode-resource:;"/>
${styles}
</head>
<body class="vscode-body 'scrollBeyondLastLine' 'wordWrap' 'showEditorSelection'" style="tab-size:4">
${head}
${info}
${body}
</body>
</html>
`;
}

protected onDidDisposeWebview(): void {
super.onDidDisposeWebview();
}

private parseSolution(raw: string): Solution {
raw = raw.slice(1); // skip first empty line
[this.problemName, raw] = raw.split(/\n\n([^]+)/); // parse problem name and skip one line
const solution: Solution = new Solution();
// [^] matches everything including \n, yet can be replaced by . in ES2018's `m` flag
[solution.title, raw] = raw.split(/\n\n([^]+)/);
[solution.url, raw] = raw.split(/\n\n([^]+)/);
[solution.lang, raw] = raw.match(/\* Lang:\s+(.+)\n([^]+)/)!.slice(1);
[solution.author, raw] = raw.match(/\* Author:\s+(.+)\n([^]+)/)!.slice(1);
[solution.votes, raw] = raw.match(/\* Votes:\s+(\d+)\n\n([^]+)/)!.slice(1);
solution.body = raw;
return solution;
}
}

// tslint:disable-next-line:max-classes-per-file
class Solution {
public title: string = "";
public url: string = "";
public lang: string = "";
public author: string = "";
public votes: string = "";
public body: string = ""; // Markdown supported
}

export const leetCodeSolutionProvider: LeetCodeSolutionProvider = new LeetCodeSolutionProvider();
106 changes: 106 additions & 0 deletions src/webview/leetCodeSubmissionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import { ViewColumn } from "vscode";
import { openKeybindingsEditor, promptHintMessage } from "../utils/uiUtils";
import { ILeetCodeWebviewOption, LeetCodeWebview } from "./LeetCodeWebview";
import { markdownEngine } from "./markdownEngine";

class LeetCodeSubmissionProvider extends LeetCodeWebview {

protected readonly viewType: string = "leetcode.submission";
private result: IResult;

public show(resultString: string): void {
this.result = this.parseResult(resultString);
this.showWebviewInternal();
this.showKeybindingsHint();
}

protected getWebviewOption(): ILeetCodeWebviewOption {
return {
title: "Submission",
viewColumn: ViewColumn.Two,
};
}

protected getWebviewContent(): string {
const styles: string = markdownEngine.getStyles();
const title: string = `## ${this.result.messages[0]}`;
const messages: string[] = this.result.messages.slice(1).map((m: string) => `* ${m}`);
const sections: string[] = Object.keys(this.result)
.filter((key: string) => key !== "messages")
.map((key: string) => [
`### ${key}`,
"```",
this.result[key].join("\n"),
"```",
].join("\n"));
const body: string = markdownEngine.render([
title,
...messages,
...sections,
].join("\n"));
return `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https:; script-src vscode-resource:; style-src vscode-resource:;"/>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
${styles}
</head>
<body class="vscode-body 'scrollBeyondLastLine' 'wordWrap' 'showEditorSelection'" style="tab-size:4">
${body}
</body>
</html>
`;
}

protected onDidDisposeWebview(): void {
super.onDidDisposeWebview();
}

private async showKeybindingsHint(): Promise<void> {
await promptHintMessage(
"hint.commandShortcut",
'You can customize shortcut key bindings in File > Preferences > Keyboard Shortcuts with query "leetcode".',
"Open Keybindings",
(): Promise<any> => openKeybindingsEditor("leetcode solution"),
);
}

private parseResult(raw: string): IResult {
raw = raw.concat(" √ "); // Append a dummy sentinel to the end of raw string
const regSplit: RegExp = / [×vx] ([^]+?)\n(?= [×vx] )/g;
const regKeyVal: RegExp = /(.+?): ([^]*)/;
const result: IResult = { messages: [] };
let entry: RegExpExecArray | null;
do {
entry = regSplit.exec(raw);
if (!entry) {
continue;
}
const kvMatch: RegExpExecArray | null = regKeyVal.exec(entry[1]);
if (kvMatch) {
const [key, value] = kvMatch.slice(1);
if (value) { // Do not show empty string
if (!result[key]) {
result[key] = [];
}
result[key].push(value);
}
} else {
result.messages.push(entry[1]);
}
} while (entry);
return result;
}
}

interface IResult {
[key: string]: string[];
messages: string[];
}

export const leetCodeSubmissionProvider: LeetCodeSubmissionProvider = new LeetCodeSubmissionProvider();
166 changes: 166 additions & 0 deletions src/webview/markdownEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) jdneo. All rights reserved.
// Licensed under the MIT license.

import * as hljs from "highlight.js";
import * as MarkdownIt from "markdown-it";
import * as os from "os";
import * as path from "path";
import * as vscode from "vscode";
import { leetCodeChannel } from "../leetCodeChannel";
import { isWindows } from "../utils/osUtils";

class MarkdownEngine implements vscode.Disposable {

private engine: MarkdownIt;
private config: MarkdownConfiguration;
private listener: vscode.Disposable;

public constructor() {
this.reload();
this.listener = vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => {
if (event.affectsConfiguration("markdown")) {
this.reload();
}
}, this);
}

public get localResourceRoots(): vscode.Uri[] {
return [vscode.Uri.file(path.join(this.config.extRoot, "media"))];
}

public dispose(): void {
this.listener.dispose();
}

public reload(): void {
this.engine = this.initEngine();
this.config = new MarkdownConfiguration();
}

public render(md: string, env?: any): string {
return this.engine.render(md, env);
}

public getStyles(): string {
return [
this.getBuiltinStyles(),
this.getSettingsStyles(),
].join(os.EOL);
}

private getBuiltinStyles(): string {
let styles: vscode.Uri[] = [];
try {
const stylePaths: string[] = require(path.join(this.config.extRoot, "package.json"))["contributes"]["markdown.previewStyles"];
styles = stylePaths.map((p: string) => vscode.Uri.file(path.join(this.config.extRoot, p)).with({ scheme: "vscode-resource" }));
} catch (error) {
leetCodeChannel.appendLine("[Error] Fail to load built-in markdown style file.");
}
return styles.map((style: vscode.Uri) => `<link rel="stylesheet" type="text/css" href="${style.toString()}">`).join(os.EOL);
}

private getSettingsStyles(): string {
return [
`<style>`,
`body {`,
` ${this.config.fontFamily ? `font-family: ${this.config.fontFamily};` : ``}`,
` ${isNaN(this.config.fontSize) ? `` : `font-size: ${this.config.fontSize}px;`}`,
` ${isNaN(this.config.lineHeight) ? `` : `line-height: ${this.config.lineHeight};`}`,
`}`,
`</style>`,
].join(os.EOL);
}

private initEngine(): MarkdownIt {
const md: MarkdownIt = new MarkdownIt({
linkify: true,
typographer: true,
highlight: (code: string, lang?: string): string => {
switch (lang && lang.toLowerCase()) {
case "mysql":
lang = "sql"; break;
case "json5":
lang = "json"; break;
case "python3":
lang = "python"; break;
}
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, code, true).value;
} catch (error) { /* do not highlight */ }
}
return ""; // use external default escaping
},
});

this.addCodeBlockHighlight(md);
this.addImageUrlCompletion(md);
this.addLinkValidator(md);
return md;
}

private addCodeBlockHighlight(md: MarkdownIt): void {
const codeBlock: MarkdownIt.TokenRender = md.renderer.rules["code_block"];
// tslint:disable-next-line:typedef
md.renderer.rules["code_block"] = (tokens, idx, options, env, self) => {
// if any token uses lang-specified code fence, then do not highlight code block
if (tokens.some((token: any) => token.type === "fence")) {
return codeBlock(tokens, idx, options, env, self);
}
// otherwise, highlight with default lang in env object.
const highlighted: string = options.highlight(tokens[idx].content, env.lang);
return [
`<pre><code ${self.renderAttrs(tokens[idx])} >`,
highlighted || md.utils.escapeHtml(tokens[idx].content),
"</code></pre>",
].join(os.EOL);
};
}

private addImageUrlCompletion(md: MarkdownIt): void {
const image: MarkdownIt.TokenRender = md.renderer.rules["image"];
// tslint:disable-next-line:typedef
md.renderer.rules["image"] = (tokens, idx, options, env, self) => {
const imageSrc: string[] | undefined = tokens[idx].attrs.find((value: string[]) => value[0] === "src");
if (env.host && imageSrc && imageSrc[1].startsWith("/")) {
imageSrc[1] = `${env.host}${imageSrc[1]}`;
}
return image(tokens, idx, options, env, self);
};
}

private addLinkValidator(md: MarkdownIt): void {
const validateLink: (link: string) => boolean = md.validateLink;
md.validateLink = (link: string): boolean => {
// support file:// protocal link
return validateLink(link) || link.startsWith("file:");
};
}
}

// tslint:disable-next-line: max-classes-per-file
class MarkdownConfiguration {

public readonly extRoot: string; // root path of vscode built-in markdown extension
public readonly lineHeight: number;
public readonly fontSize: number;
public readonly fontFamily: string;

public constructor() {
const markdownConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("markdown", null);
this.extRoot = path.join(vscode.env.appRoot, "extensions", "markdown-language-features");
this.lineHeight = Math.max(0.6, +markdownConfig.get<number>("preview.lineHeight", NaN));
this.fontSize = Math.max(8, +markdownConfig.get<number>("preview.fontSize", NaN));
this.fontFamily = this.resolveFontFamily(markdownConfig);
}

private resolveFontFamily(config: vscode.WorkspaceConfiguration): string {
let fontFamily: string = config.get<string>("preview.fontFamily", "");
if (isWindows() && fontFamily === config.inspect<string>("preview.fontFamily")!.defaultValue) {
fontFamily = `${fontFamily}, 'Microsoft Yahei UI'`;
}
return fontFamily;
}
}

export const markdownEngine: MarkdownEngine = new MarkdownEngine();
236 changes: 236 additions & 0 deletions thirdpartynotice.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
For vscode-leetcode extension

This extension uses Open Source components. You can find the source code of their
open source projects along with the license information below. We acknowledge and
are grateful to these developers for their contribution to open source.

1. fs-extra (https://github.com/jprichardson/node-fs-extra)
2. highlight.js (https://github.com/highlightjs/highlight.js/)
3. require-from-string (https://github.com/floatdrop/require-from-string)
4. lodash (https://github.com/lodash/lodash)
5. markdown-it (https://github.com/markdown-it/markdown-it)
6. leetcode-cli (https://github.com/skygragon/leetcode-cli)
7. unescape-js (https://github.com/iamakulov/unescape-js)

fs-extra NOTICES BEGIN HERE
=============================

(The MIT License)

Copyright (c) 2011-2017 JP Richardson

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

END OF fs-extra NOTICES AND INFORMATION
==================================

highlight.js NOTICES BEGIN HERE
=============================

Copyright (c) 2006, Ivan Sagalaev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of highlight.js nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

END OF highlight.js NOTICES AND INFORMATION
==================================

require-from-string NOTICES BEGIN HERE
=============================

The MIT License (MIT)

Copyright (c) Vsevolod Strukchinsky <floatdrop@gmail.com> (github.com/floatdrop)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

END OF require-from-string NOTICES AND INFORMATION
==================================

lodash NOTICES BEGIN HERE
=============================

The MIT License

Copyright JS Foundation and other contributors <https://js.foundation/>

Based on Underscore.js, copyright Jeremy Ashkenas,
DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>

This software consists of voluntary contributions made by many
individuals. For exact contribution history, see the revision history
available at https://github.com/lodash/lodash

The following license applies to all parts of this software except as
documented below:

====

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

====

Copyright and related rights for sample code are waived via CC0. Sample
code is defined as all source code displayed within the prose of the
documentation.

CC0: http://creativecommons.org/publicdomain/zero/1.0/

====

Files located in the node_modules and vendor directories are externally
maintained libraries used by this software which have their own
licenses; we recommend you read them, as their terms may differ from the
terms above.

END OF lodash NOTICES AND INFORMATION
==================================

markdown-it NOTICES BEGIN HERE
=============================

Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

END OF markdown-it NOTICES AND INFORMATION
==================================

leetcode-cli NOTICES BEGIN HERE
=============================

MIT License

Copyright (c) 2016 skygragon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

END OF leetcode-cli NOTICES AND INFORMATION
==================================

unescape-js NOTICES BEGIN HERE
=============================

The MIT License (MIT)

Copyright (c) Ivan Akulov <mail@iamakulov.com> (http://iamakulov.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

END OF unescape-js NOTICES AND INFORMATION
==================================
39 changes: 18 additions & 21 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"object-literal-sort-keys": false,
"indent": [
true,
"spaces"
],
"ordered-imports": [false],
"indent": [true, "spaces"],
"no-string-literal": false,
"no-namespace": false,
"max-line-length": [
false,
120
],
"typedef": [
true,
"call-signature",
"arrow-call-signature",
"parameter",
"arrow-parameter",
"property-declaration",
"variable-declaration",
"member-variable-declaration"
]
"max-line-length": [false, 120],
"typedef": false,
"no-implicit-dependencies": [true, ["vscode"]],
"trailing-comma": false,
"no-any": false,
"object-literal-key-quotes": [true, "consistent-as-needed"],
"prefer-object-spread": false,
"no-unnecessary-await": false,
"semicolon": [false],
"quotemark": [false],
"member-ordering": [false],
"variable-name": [false],
"curly": false,
"interface-over-type-literal": [false],
"no-unused-expression": false
},
"rulesDirectory": []
}