├── template ├── cpp │ ├── algm.h │ ├── ListNode.h │ ├── TreeNode.h │ └── parse_test.cpp ├── golang │ ├── go.mod │ ├── go.sum │ ├── main_test.go │ └── algm.go └── java │ ├── ListNode.java │ └── TreeNode.java ├── images ├── copy.png ├── memo.gif ├── test.png ├── build.png ├── debug.gif ├── debug.png ├── history.png ├── search.gif ├── algorithm.png ├── shortcut.png └── todayRecord.gif ├── .gitignore ├── .prettierrc.json ├── .vscodeignore ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── media ├── dark │ └── copy.svg ├── light │ └── copy.svg ├── buildcode.js ├── ac.svg ├── algorithm.svg ├── history.css ├── highlight.css └── markdown.css ├── src ├── model │ ├── memo.ts │ ├── command.ts │ ├── question.cn.ts │ ├── common.ts │ └── question.ts ├── fileStorage │ ├── README.md │ ├── util.ts │ ├── crc32_table.ts │ └── db.ts ├── test │ ├── suite │ │ ├── extension.test.ts │ │ ├── index.ts │ │ └── util.test.ts │ ├── runTest.ts │ └── unit │ │ ├── test.ts │ │ └── fileStorage.test.ts ├── db.ts ├── common │ ├── website.ts │ ├── lang.ts │ ├── transformCode.ts │ ├── langConfig.ts │ └── parseContent.ts ├── process.ts ├── provider │ ├── completionProvider.ts │ ├── snippetProvider.ts │ ├── questionsProvider.ts │ ├── resolver.cn.ts │ ├── resolver.ts │ ├── codelensProvider.ts │ └── memoProvider.ts ├── execTestCode.ts ├── lang │ ├── bash.ts │ ├── database.ts │ ├── common.ts │ ├── javascript.ts │ └── typescript.ts ├── memo │ └── index.ts ├── debugTask │ └── index.ts ├── cache.ts ├── history │ ├── storage.ts │ ├── answer.ts │ └── index.ts ├── api │ └── index.ts ├── extension.ts ├── babelPlugin.ts ├── debug │ └── launch.ts ├── child_process │ └── execTestCode.ts ├── webview │ ├── buildCodeWebview.ts │ └── submitHistory.ts └── login │ └── index.ts ├── tsconfig.json ├── .eslintrc.json └── docs └── README_zh-CN.md /template/cpp/algm.h: -------------------------------------------------------------------------------- 1 | #include "ListNode.h" 2 | #include "TreeNode.h" -------------------------------------------------------------------------------- /images/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/copy.png -------------------------------------------------------------------------------- /images/memo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/memo.gif -------------------------------------------------------------------------------- /images/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/test.png -------------------------------------------------------------------------------- /images/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/build.png -------------------------------------------------------------------------------- /images/debug.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/debug.gif -------------------------------------------------------------------------------- /images/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/debug.png -------------------------------------------------------------------------------- /images/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/history.png -------------------------------------------------------------------------------- /images/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/search.gif -------------------------------------------------------------------------------- /images/algorithm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/algorithm.png -------------------------------------------------------------------------------- /images/shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/shortcut.png -------------------------------------------------------------------------------- /images/todayRecord.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supperchong/algorithm/HEAD/images/todayRecord.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | yarn-error.log 6 | yarn.lock 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /template/golang/go.mod: -------------------------------------------------------------------------------- 1 | module algm 2 | 3 | go 1.13 4 | 5 | require github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": false, 5 | "singleQuote": true, 6 | "useTabs": true, 7 | "printWidth": 120 8 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | .gitignore 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /template/golang/go.sum: -------------------------------------------------------------------------------- 1 | github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 h1:cZqz+yOJ/R64LcKjNQOdARott/jP7BnUQ9Ah7KaZCvw= 2 | github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= 3 | -------------------------------------------------------------------------------- /media/dark/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /media/light/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /template/cpp/ListNode.h: -------------------------------------------------------------------------------- 1 | 2 | //Definition for singly-linked list. 3 | #ifndef _LISTNODE 4 | #define _LISTNODE 5 | struct ListNode { 6 | int val; 7 | ListNode *next; 8 | ListNode() : val(0), next(nullptr) {} 9 | ListNode(int x) : val(x), next(nullptr) {} 10 | ListNode(int x, ListNode *next) : val(x), next(next) {} 11 | }; 12 | #endif -------------------------------------------------------------------------------- /src/model/memo.ts: -------------------------------------------------------------------------------- 1 | import { ResolverParam } from '../provider/resolver' 2 | 3 | interface MemoQuestionFile { 4 | type: 'question' 5 | name: string 6 | param: Partial 7 | } 8 | interface MemoOtherFile { 9 | type: 'other' 10 | name: string 11 | param: string 12 | } 13 | export type MemoFile = MemoQuestionFile | MemoOtherFile 14 | export interface MemoFolder { 15 | name: string 16 | children: MemoFile[] 17 | } 18 | -------------------------------------------------------------------------------- /template/cpp/TreeNode.h: -------------------------------------------------------------------------------- 1 | 2 | // Definition for a binary tree node. 3 | #ifndef _TREENODE 4 | #define _TREENODE 5 | struct TreeNode 6 | { 7 | int val; 8 | TreeNode *left; 9 | TreeNode *right; 10 | TreeNode() : val(0), left(nullptr), right(nullptr) {} 11 | TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} 12 | TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} 13 | }; 14 | #endif -------------------------------------------------------------------------------- /media/buildcode.js: -------------------------------------------------------------------------------- 1 | function renderCode(code){ 2 | const buildCodeDiv=document.getElementById('buildCode') 3 | buildCodeDiv.innerHTML=code 4 | } 5 | window.addEventListener("message", (event) => { 6 | const message = event.data; // The JSON data our extension sent 7 | 8 | switch (message.command) { 9 | case "newCode": { 10 | renderCode(message.data) 11 | break; 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /template/java/ListNode.java: -------------------------------------------------------------------------------- 1 | package algm; 2 | 3 | // Definition for singly-linked list. 4 | public class ListNode { 5 | public int val; 6 | public ListNode next; 7 | 8 | public ListNode() { 9 | } 10 | 11 | public ListNode(int val) { 12 | this.val = val; 13 | } 14 | 15 | public ListNode(int val, ListNode next) { 16 | this.val = val; 17 | this.next = next; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/fileStorage/README.md: -------------------------------------------------------------------------------- 1 | # 存储文件格式 2 | 3 | ## header 4 | 5 | | field | length | 6 | | ------------ | ------ | 7 | | magic number | 32 | 8 | | version | 32 | 9 | | checksum | 32 | 10 | | count | 32 | 11 | 12 | ### section header 13 | 14 | | field | length | 15 | | --------- | ------ | 16 | | checksum | 32 | 17 | | id | 32 | 18 | | fid | 32 | 19 | | offset | 32 | 20 | | size | 16 | 21 | | hash | 128 | 22 | | timestamp | 64 | 23 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode' 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.') 10 | 11 | test('Sample test', () => { 12 | assert.equal(-1, [1, 2, 3].indexOf(5)) 13 | assert.equal(-1, [1, 2, 3].indexOf(0)) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /template/java/TreeNode.java: -------------------------------------------------------------------------------- 1 | package algm; 2 | 3 | // Definition for a binary tree node. 4 | public class TreeNode { 5 | public int val; 6 | public TreeNode left; 7 | public TreeNode right; 8 | 9 | public TreeNode() { 10 | } 11 | 12 | public TreeNode(int val) { 13 | this.val = val; 14 | } 15 | 16 | public TreeNode(int val, TreeNode left, TreeNode right) { 17 | this.val = val; 18 | this.left = left; 19 | this.right = right; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "git.rebaseWhenSync": true 12 | } -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import Db from './fileStorage/db' 2 | import { config } from './config' 3 | import { Lang } from './model/common' 4 | 5 | const filename = 'question' 6 | const dbMap: Partial> = {} 7 | export async function getDb(): Promise { 8 | const lang = config.lang 9 | const db = dbMap[lang] 10 | if (db) { 11 | return db 12 | } else { 13 | const dbDir = config.dbDir 14 | const db = new Db({ 15 | dbDir, 16 | filename: filename, 17 | }) 18 | await db.init() 19 | dbMap[lang] = db 20 | return db 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /media/ac.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/common/website.ts: -------------------------------------------------------------------------------- 1 | import { Lang } from '../model/common' 2 | import { config, DomainCN, DomainEN } from '../config' 3 | export enum Website { 4 | Leetcode = 'https://leetcode.com', 5 | LeetcodeCn = 'https://leetcode.cn', 6 | } 7 | export const WebsiteMap = { 8 | [Lang.cn]: Website.LeetcodeCn, 9 | [Lang.en]: Website.Leetcode, 10 | } 11 | function baseTag(strs: TemplateStringsArray, ...arr: string[]): string { 12 | const baseUrl = WebsiteMap[config.lang] 13 | return baseUrl + '/' + arr.reduce((prev, cur, i) => prev + cur + strs[i + 1], strs[0]) 14 | } 15 | export const getSolution = (titleSlug: string): string => baseTag`problems/${titleSlug}/solution/` 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "test", 22 | "group": "test", 23 | "problemMatcher": [] 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "compile", 28 | "group": "build", 29 | "problemMatcher": [] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | import { runTests } from 'vscode-test' 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../') 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index') 14 | // Download VS Code, unzip it and run the integration test 15 | await runTests({ extensionDevelopmentPath, extensionTestsPath }) 16 | } catch (err) { 17 | console.error('Failed to run tests') 18 | process.exit(1) 19 | } 20 | } 21 | 22 | main() 23 | -------------------------------------------------------------------------------- /src/process.ts: -------------------------------------------------------------------------------- 1 | import * as cp from 'child_process' 2 | 3 | 4 | export const processArr: cp.ChildProcess[] = [] 5 | export function clearProcessArr() { 6 | processArr.forEach(p => { 7 | p.kill() 8 | }) 9 | } 10 | 11 | class ChildProcessProxy { 12 | processIdSet: Set = new Set() 13 | clear() { 14 | this.processIdSet.forEach(pid => { 15 | process.kill(pid) 16 | }) 17 | } 18 | add(p: cp.ChildProcess) { 19 | const pid = p.pid 20 | this.processIdSet.add(pid) 21 | p.once('exit', () => { 22 | this.processIdSet.delete(pid) 23 | }) 24 | } 25 | remove(p: cp.ChildProcess | null) { 26 | if (!p) { 27 | return 28 | } 29 | if (this.processIdSet.has(p.pid)) { 30 | p.kill() 31 | } 32 | } 33 | } 34 | 35 | export const childProcessProxy = new ChildProcessProxy() -------------------------------------------------------------------------------- /src/common/lang.ts: -------------------------------------------------------------------------------- 1 | import { CaseList } from './util' 2 | 3 | export type MetaData = LanguageMetaData | DataBaseMetaData | ShellMetaData 4 | interface Param { 5 | name: string 6 | type: string 7 | } 8 | interface ReturnType { 9 | type: string 10 | } 11 | export interface LanguageMetaData { 12 | name: string 13 | params: Param[] 14 | return: ReturnType 15 | } 16 | export interface DataBaseMetaData { 17 | database: true 18 | mssql: string[] 19 | mysql: string[] 20 | oraclesql: string[] 21 | } 22 | export interface ShellMetaData { 23 | shell: true 24 | } 25 | 26 | export interface TestOptions { 27 | caseList: CaseList 28 | metaData: LanguageMetaData 29 | filePath: string 30 | originCode: string 31 | } 32 | export type DebugOptions = Omit 33 | export const defaultTimeout = 10000 34 | -------------------------------------------------------------------------------- /src/fileStorage/util.ts: -------------------------------------------------------------------------------- 1 | import crypto = require('crypto') 2 | import crc32_table from './crc32_table' 3 | export function md5(data: crypto.BinaryLike): string { 4 | const hash = crypto.createHash('md5') 5 | return hash.update(data).digest('hex') 6 | } 7 | 8 | /** 9 | * Reference:https://docs.microsoft.com/en-us/openspecs/office_protocols/ms-abs/06966aa2-70da-4bf9-8448-3355f277cd77?redirectedfrom=MSDN 10 | * @param buffer 11 | */ 12 | export function crc32(buffer: Buffer): number { 13 | let crc = 0xffffffff 14 | let i = 0 15 | while (i < buffer.length) { 16 | crc = crc32_table[(crc & 0xff) ^ buffer[i]] ^ ((crc >= 0 ? crc : 2 ** 32 + crc) / 256) 17 | i++ 18 | } 19 | crc = crc ^ 0xffffffff 20 | return crc >= 0 ? crc : 2 ** 32 + crc 21 | } 22 | export function verify(data: Buffer, checksum: number) { 23 | return crc32(data) === checksum 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "useUnknownInCatchVariables":false, 10 | "sourceMap": true, 11 | "noImplicitAny": false, 12 | "noImplicitThis": false, 13 | "rootDir": "src", 14 | "strict": true /* enable all strict type-checking options */ 15 | /* Additional Checks */ 16 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 17 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 18 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | ".vscode-test" 23 | ] 24 | } -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as Mocha from 'mocha' 3 | import * as glob from 'glob' 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | color: true, 10 | }) 11 | 12 | const testsRoot = path.resolve(__dirname, '..') 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err) 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))) 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run((failures) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)) 28 | } else { 29 | c() 30 | } 31 | }) 32 | } catch (err) { 33 | console.error(err) 34 | e(err) 35 | } 36 | }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/provider/completionProvider.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../config' 2 | import * as vscode from 'vscode' 3 | 4 | const defaultCompletionItems = [ 5 | { 6 | languages: ['javascript', 'typescript'], 7 | prefix: 'algorithm', 8 | body: "// @algorithm\r\nimport * as a from '" + config.algmModuleDir + "'\r\n\r\n", 9 | }, 10 | ] 11 | export function registerCompletionItemProvider(context: vscode.ExtensionContext) { 12 | defaultCompletionItems.forEach(({ languages, prefix, body }) => { 13 | context.subscriptions.push( 14 | vscode.languages.registerCompletionItemProvider( 15 | languages, 16 | { 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { 19 | const snippetCompletion = new vscode.CompletionItem(prefix) 20 | snippetCompletion.insertText = new vscode.SnippetString(body) 21 | 22 | return [snippetCompletion] 23 | }, 24 | }, 25 | '.' // triggered whenever a '.' is being typed 26 | ) 27 | ) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | // "preLaunchTask": "${defaultBuildTask}" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "${defaultBuildTask}" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/test/unit/test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | // import * as myExtension from '../../extension'; 6 | import { getTestCaseList } from '../../common/util' 7 | const cases = [ 8 | `// @algorithm @lc id=1576 lang=javascript weekname=weekly-contest-191 9 | // @title reorder-routes-to-make-all-paths-lead-to-the-city-zero 10 | 11 | // @test(6,[[0,1],[1,3],[2,3],[4,0],[4,5]])=3 12 | // @test(5,[[1,0],[1,2],[3,2],[3,4]])=2 13 | // @test(3,[[1,0],[2,0]])=0 14 | /** 15 | * @param {number} n 16 | * @param {number[][]} connections 17 | * @return {number} 18 | */ 19 | var minReorder = function (n, connections) { 20 | } 21 | `, 22 | ] 23 | suite('Util Test Suite', () => { 24 | test('test getTestCaseList', () => { 25 | const testCaseList = getTestCaseList(cases[0]) 26 | assert.equal(testCaseList.length, 1) 27 | const { testCase } = testCaseList[0] 28 | assert.deepEqual(testCase, [ 29 | '// @test(6,[[0,1],[1,3],[2,3],[4,0],[4,5]])=3', 30 | '// @test(5,[[1,0],[1,2],[3,2],[3,4]])=2', 31 | '// @test(3,[[1,0],[2,0]])=0', 32 | ]) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/model/command.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { TestCaseParam } from '../common/util' 3 | import { QuestionsProvider } from '../provider/questionsProvider' 4 | 5 | export interface CodeLensesOptions { 6 | text: string 7 | filePath: string 8 | langSlug: string 9 | } 10 | export interface TestCodeLensesOptions { 11 | filePath: string 12 | testCaseParam: TestCaseParam 13 | } 14 | export type BuildCodeLensesOptions = CodeLensesOptions 15 | export type SubmitCodeLensesOptions = CodeLensesOptions 16 | export type GetDescriptionCodeLensesOptions = CodeLensesOptions 17 | export type ViewSubmitHistoryCodeLensesOptions = CodeLensesOptions 18 | type FilePath = string 19 | export type DebugLensesOptions = FilePath 20 | type ExtensionPath = string 21 | export type BuildCodeArguments = [vscode.ExtensionContext, BuildCodeLensesOptions] 22 | export type TestCodeArguments = [TestCodeLensesOptions] 23 | export type DebugCodeArguments = [DebugLensesOptions] 24 | export type SubmitCodeArguments = [QuestionsProvider, SubmitCodeLensesOptions] 25 | export type GetDescriptionArguments = [ExtensionPath, GetDescriptionCodeLensesOptions] 26 | export type ViewSubmitHistoryArguments = [vscode.ExtensionContext, CodeLensesOptions] 27 | -------------------------------------------------------------------------------- /src/execTestCode.ts: -------------------------------------------------------------------------------- 1 | import cp = require('child_process') 2 | 3 | import path = require('path') 4 | import { config } from './config' 5 | import { window } from 'vscode' 6 | import { TestOptions } from './common/lang' 7 | const notFoundReg = /node: not found/ 8 | export function execTestChildProcess(options: TestOptions): Promise { 9 | return new Promise((resolve) => { 10 | const { nodeBinPath } = config 11 | const cmd = cp.spawn(nodeBinPath, [path.join(__dirname, './child_process/execTestCode.js')]) 12 | let msg = '' 13 | cmd.stdout.on('data', (data) => { 14 | console.log('data', data) 15 | msg += data 16 | }) 17 | cmd.stdin.setDefaultEncoding('utf8') 18 | cmd.stdin.write(JSON.stringify(options)) 19 | cmd.stdin.end() 20 | // cmd.send(JSON.stringify(options)) 21 | cmd.stderr.on('data', (message) => { 22 | console.log('err', message.toString()) 23 | if (notFoundReg.test(message)) { 24 | window.showErrorMessage(message + ',please set the path of the nodejs') 25 | } else { 26 | window.showErrorMessage(message) 27 | } 28 | }) 29 | cmd.on('close', () => { 30 | resolve(msg) 31 | console.log('child process close') 32 | console.log('msg', msg) 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/lang/bash.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../config' 2 | import { OutputChannel } from 'vscode' 3 | import { readFileAsync } from '../common/util' 4 | import { getFuncNames } from '../common/util' 5 | 6 | export class BashParse { 7 | public log: OutputChannel 8 | public originCode?: string 9 | public commentToken = '#' 10 | constructor(public filePath: string, public text?: string) { 11 | this.log = log 12 | if (!filePath) { 13 | throw new Error('filePath must not empty') 14 | } 15 | if (text) { 16 | this.originCode = text 17 | } 18 | } 19 | async getOriginCode() { 20 | if (this.originCode) { 21 | return this.originCode 22 | } 23 | const originCode = await readFileAsync(this.filePath, { 24 | encoding: 'utf8', 25 | }) 26 | this.originCode = originCode 27 | return this.originCode 28 | } 29 | public async buildCode() { 30 | const originCode = await this.getOriginCode() 31 | const { questionMeta } = getFuncNames(originCode, this.filePath) 32 | 33 | const code = originCode 34 | .split('\n') 35 | .filter((line) => !this.isComment(line)) 36 | .join('\n') 37 | return { 38 | code, 39 | questionMeta, 40 | } 41 | } 42 | isComment(line: string): boolean { 43 | return line.trim().startsWith('#') 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/lang/database.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../config' 2 | import { OutputChannel } from 'vscode' 3 | import { readFileAsync } from '../common/util' 4 | import { getFuncNames } from '../common/util' 5 | 6 | export class DataBaseParse { 7 | public log: OutputChannel 8 | public originCode?: string 9 | public commentToken = '#' 10 | constructor(public filePath: string, public text?: string) { 11 | this.log = log 12 | if (!filePath) { 13 | throw new Error('filePath must not empty') 14 | } 15 | if (text) { 16 | this.originCode = text 17 | } 18 | } 19 | async getOriginCode() { 20 | if (this.originCode) { 21 | return this.originCode 22 | } 23 | const originCode = await readFileAsync(this.filePath, { 24 | encoding: 'utf8', 25 | }) 26 | this.originCode = originCode 27 | return this.originCode 28 | } 29 | public async buildCode() { 30 | const originCode = await this.getOriginCode() 31 | const { questionMeta } = getFuncNames(originCode, this.filePath) 32 | 33 | const code = originCode 34 | .split('\n') 35 | .filter((line) => !this.isComment(line)) 36 | .join('\n') 37 | return { 38 | code, 39 | questionMeta, 40 | } 41 | } 42 | isComment(line: string): boolean { 43 | return line.trim().startsWith('#') 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/unit/fileStorage.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | // import * as myExtension from '../../extension'; 6 | import { crc32 } from '../../fileStorage/util' 7 | import { convertToObj, convertToBuffer, Field } from '../../fileStorage/db' 8 | interface FieldData { 9 | v: number 10 | h: string 11 | } 12 | suite('test file storage', () => { 13 | test('test crc32', () => { 14 | const data = Buffer.from([0x61, 0x62, 0x63]) 15 | 16 | const result = crc32(data) 17 | assert.equal(result, 0x352441c2) 18 | }) 19 | test('test convertToObj', () => { 20 | const config: Field[] = [ 21 | { 22 | field: 'v', 23 | type: 'int', 24 | len: 16, 25 | }, 26 | { 27 | field: 'h', 28 | type: 'hex', 29 | len: 32, 30 | }, 31 | ] 32 | const buffer = Buffer.from([0x00, 0x11, 0x00, 0x12, 0xab, 0xcd]) 33 | const result = convertToObj(buffer, config) 34 | assert.equal(result.v, 17) 35 | assert.equal(result.h, '0012abcd') 36 | }) 37 | test('test convertToBuffer', () => { 38 | const config: Field[] = [ 39 | { 40 | field: 'v', 41 | type: 'int', 42 | len: 16, 43 | }, 44 | { 45 | field: 'h', 46 | type: 'hex', 47 | len: 32, 48 | }, 49 | ] 50 | const obj: FieldData = { 51 | v: 17, 52 | h: '0012abcd', 53 | } 54 | const buffer = Buffer.from([0x00, 0x11, 0x00, 0x12, 0xab, 0xcd]) 55 | const result = convertToBuffer(obj, config) 56 | assert.deepEqual(result, buffer) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/common/transformCode.ts: -------------------------------------------------------------------------------- 1 | import * as recast from 'recast' 2 | import * as acorn from 'acorn' 3 | import { builders, namedTypes } from 'ast-types' 4 | import { FunctionDeclarationKind, VariableDeclarationKind } from 'ast-types/gen/kinds' 5 | 6 | function addAstComment(ast: namedTypes.File, comment: string, funName: string) { 7 | const visitor = { 8 | FunctionDeclaration: (node: FunctionDeclarationKind) => { 9 | if (node.id && node.id.name === funName) { 10 | insertLineComment(node, comment) 11 | } 12 | }, 13 | VariableDeclaration: (node: VariableDeclarationKind) => { 14 | if ( 15 | node.declarations.find((declaration) => { 16 | return ( 17 | declaration.type === 'VariableDeclarator' && 18 | declaration.id.type === 'Identifier' && 19 | declaration.id.name === funName 20 | ) 21 | }) 22 | ) { 23 | insertLineComment(node, comment) 24 | } 25 | }, 26 | } 27 | const body = ast.program.body 28 | for (const node of body) { 29 | const fn = visitor[node.type] 30 | if (fn) { 31 | fn(node) 32 | } 33 | } 34 | } 35 | function insertLineComment(node: VariableDeclarationKind | FunctionDeclarationKind, comment: string) { 36 | const commentNode = builders.commentLine(comment, true) 37 | const originComments = node.comments || [] 38 | const mergeComments = [commentNode, ...originComments] 39 | 40 | node.comments = mergeComments 41 | } 42 | export function addComment(source: string, comment: string, funName: string) { 43 | const ast: namedTypes.File = recast.parse(source, { 44 | parser: acorn, 45 | }) 46 | addAstComment(ast, comment, funName) 47 | const output = recast.print(ast).code 48 | return output 49 | } 50 | -------------------------------------------------------------------------------- /src/provider/snippetProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { setLinePrefix } from '../common/util' 3 | import { tag } from 'pretty-tag' 4 | export function registerForSnippetProviders(context: vscode.ExtensionContext) { 5 | const forVariables = ['i', 'j', 'k'] 6 | const fns = forVariables.map((forVariable) => { 7 | return function provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) { 8 | const key = `for${forVariable}.` 9 | let text = document.lineAt(position).text 10 | if (!text.endsWith(key)) { 11 | return 12 | } 13 | let whitePrefix = '' 14 | for (let i = 0; i < text.length; i++) { 15 | if (!/\s/.test(text[i])) { 16 | whitePrefix = text.slice(0, i) 17 | break 18 | } 19 | } 20 | text = text.trimLeft() 21 | const snippetCompletion = new vscode.CompletionItem(text) 22 | const range = document.lineAt(position).range 23 | snippetCompletion.range = range 24 | const prefix = text.slice(0, text.length - 5) 25 | let snippetString = tag` 26 | for (let ${forVariable} = 0; ${forVariable} < ${prefix}.length; ${forVariable}++) { 27 | let element = ${prefix}[${forVariable}] 28 | $0 29 | }` 30 | snippetString = setLinePrefix(snippetString, whitePrefix) 31 | snippetCompletion.insertText = new vscode.SnippetString(snippetString) 32 | return [snippetCompletion] 33 | } 34 | }) 35 | fns.forEach((fn) => { 36 | context.subscriptions.push( 37 | vscode.languages.registerCompletionItemProvider( 38 | { language: 'javascript', scheme: 'file' }, 39 | { provideCompletionItems: fn }, 40 | '.' 41 | ) 42 | ) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "prettier" 11 | ], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "prettier" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/semi": "off", 20 | "@typescript-eslint/explicit-function-return-type": "off", 21 | "@typescript-eslint/camelcase": ["off"], 22 | "@typescript-eslint/no-use-before-define": ["error", { "functions": false }], 23 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 24 | "@typescript-eslint/no-non-null-assertion":["off"], 25 | "@typescript-eslint/explicit-module-boundary-types":["off"], 26 | "@typescript-eslint/naming-convention": [ 27 | "error", 28 | { 29 | "selector": [ 30 | "classProperty", 31 | "objectLiteralProperty", 32 | "typeProperty", 33 | "classMethod", 34 | "objectLiteralMethod", 35 | "typeMethod", 36 | "accessor", 37 | "enumMember" 38 | ], 39 | "format": null, 40 | "modifiers": ["requiresQuotes"] 41 | } 42 | ], 43 | "curly": "error", 44 | "eqeqeq": "error", 45 | "no-throw-literal": "error", 46 | "semi": [ 47 | "error", 48 | "never" 49 | ], 50 | "indent": [ 51 | "error", 52 | "tab", 53 | { 54 | "SwitchCase": 1, 55 | "ignoredNodes": [ 56 | "ConditionalExpression" 57 | ] 58 | } 59 | ], 60 | "prefer-const": [ 61 | "error", 62 | { 63 | "destructuring": "all" 64 | } 65 | ] 66 | } 67 | } -------------------------------------------------------------------------------- /src/memo/index.ts: -------------------------------------------------------------------------------- 1 | import { existDir, writeFileAsync } from '../common/util' 2 | import { config, updateEnv } from '../config' 3 | import * as path from 'path' 4 | import { ResolverParam } from '../provider/resolver' 5 | export function addFolder(name: string) { 6 | if (config.env.memo.find((v) => v.name === name)) { 7 | throw new Error('folder already exists') 8 | } 9 | config.env.memo.push({ 10 | name, 11 | children: [], 12 | }) 13 | updateEnv('memo', config.env.memo) 14 | } 15 | export function deleteFolder(name: string) { 16 | const memo = config.env.memo 17 | const index = memo.findIndex((item) => item.name === name) 18 | memo.splice(index, 1) 19 | updateEnv('memo', memo) 20 | } 21 | export async function addFolderFile(folderName: string, fileName: string) { 22 | const memo = config.env.memo 23 | const folder = memo.find((item) => item.name === folderName) 24 | if (folder) { 25 | folder.children.push({ 26 | type: 'other', 27 | name: fileName, 28 | param: fileName, 29 | }) 30 | const memoDir = path.join(config.cacheBaseDir, 'memo') 31 | const filePath = path.join(memoDir, fileName) 32 | await existDir(memoDir) 33 | await writeFileAsync(filePath, Buffer.alloc(0)) 34 | } 35 | } 36 | export function addQuestion(folderName: string, name: string, param: Partial) { 37 | const memo = config.env.memo 38 | const folder = memo.find((item) => item.name === folderName) 39 | if (folder) { 40 | if (folder.children.find((v) => v.name === name)) { 41 | throw new Error('file already exists') 42 | } 43 | folder.children.push({ 44 | type: 'question', 45 | name, 46 | param, 47 | }) 48 | updateEnv('memo', memo) 49 | } 50 | } 51 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 52 | async function deleteFile(folderName: string, fileName: string) { 53 | const memo = config.env.memo 54 | const folder = memo.find((item) => item.name === folderName) 55 | if (folder) { 56 | const index = folder.children.findIndex((c) => c.name === fileName) 57 | if (index !== -1) { 58 | folder.children.splice(index, 1) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /media/algorithm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/history.css: -------------------------------------------------------------------------------- 1 | input:disabled { 2 | border-color: rgba(0, 0, 0, 0); 3 | background-color: rgba(0, 0, 0, 0); 4 | color: var(--vscode-list-inactiveSelectionForeground); 5 | } 6 | input { 7 | text-align: center; 8 | } 9 | .container { 10 | margin-top: 10px; 11 | height: 50vh; 12 | border-bottom: 1px solid #aaa; 13 | overflow: scroll; 14 | } 15 | .hide { 16 | display: none; 17 | } 18 | .history-code { 19 | overflow: scroll; 20 | } 21 | .nav { 22 | display: flex; 23 | } 24 | 25 | .nav button { 26 | margin-right: 20px; 27 | } 28 | .nav button.active { 29 | background-color: var(--vscode-pickerGroup-foreground); 30 | border-color: var(--vscode-pickerGroup-foreground); 31 | border-style: none; 32 | outline: none; 33 | } 34 | .nav button:focus { 35 | outline: none; 36 | } 37 | .item, 38 | .item-header { 39 | display: flex; 40 | flex-direction: row; 41 | /* justify-content: space-around; */ 42 | height: 30px; 43 | border-bottom: 1px solid #eee; 44 | align-items: center; 45 | margin-left: 10px; 46 | margin-right: 10px; 47 | font-size: 16px; 48 | border-top: 1px solid #eee; 49 | } 50 | 51 | .item > div, 52 | .item-header > div { 53 | flex: 1; 54 | text-align: center; 55 | } 56 | .item input { 57 | width: 100%; 58 | } 59 | .item { 60 | font-size: 14px; 61 | cursor: pointer; 62 | color: var(--vscode-list-inactiveSelectionForeground); 63 | background-color: var(--vscode-list-inactiveSelectionBackground); 64 | } 65 | 66 | .item:hover { 67 | background-color: var(--vscode-list-hoverBackground); 68 | color: var(--vscode-list-hoverForeground); 69 | } 70 | 71 | .item.active { 72 | background-color: var(--vscode-list-activeSelectionBackground); 73 | color: var(--vscode-list-activeSelectionForeground); 74 | } 75 | 76 | .item > div { 77 | opacity: 0.7; 78 | } 79 | .operate-box { 80 | display: flex; 81 | justify-content: space-evenly; 82 | color: var(--vscode-editorLightBulb-foreground); 83 | } 84 | 85 | body.vscode-light .operate-box { 86 | color: var(--vscode-editorLink-activeForeground); 87 | } 88 | body.vscode-light .active .operate-box { 89 | color: var(--vscode-list-activeSelectionForeground); 90 | } 91 | -------------------------------------------------------------------------------- /src/lang/common.ts: -------------------------------------------------------------------------------- 1 | import { CodeLang, getFileLang, ExtraType } from '../common/langConfig' 2 | import { BaseLang } from './base' 3 | import { PythonParse } from './python' 4 | import { GoParse } from './golang' 5 | import { JavaParse } from './java' 6 | import { CppParse } from './cpp' 7 | import { TestCase } from '../common/util' 8 | import * as vscode from 'vscode' 9 | import * as path from 'path' 10 | import { enableLang } from '../common/langConfig' 11 | import { JavascriptParse } from './javascript' 12 | import { TypescriptParse } from './typescript' 13 | const langMap = { 14 | [CodeLang.Python3]: PythonParse, 15 | [CodeLang.Go]: GoParse, 16 | [CodeLang.Java]: JavaParse, 17 | [CodeLang['C++']]: CppParse, 18 | [CodeLang.JavaScript]: JavascriptParse, 19 | [CodeLang.TypeScript]: TypescriptParse, 20 | } 21 | export class Service { 22 | public codeLang: CodeLang 23 | private ctx: BaseLang 24 | constructor(public filePath: string, public text?: string) { 25 | this.codeLang = getFileLang(filePath) 26 | 27 | const Parse = langMap[this.codeLang] 28 | if (!Parse) { 29 | const fileParse = path.parse(filePath) 30 | throw new Error('Currently, not support ' + fileParse.ext) 31 | } 32 | this.ctx = new Parse(filePath, text) 33 | } 34 | static getPreImport(codeLang: CodeLang, name: string, extraTypeSet: Set): string { 35 | const Parse = langMap[codeLang] 36 | if (!Parse) { 37 | return '' 38 | } 39 | return Parse.getPreImport(name, extraTypeSet) 40 | } 41 | static handlePreImport(filePath: string) { 42 | const codeLang = getFileLang(filePath) 43 | if (['JavaScript', 'TypeScript'].includes(codeLang)) { 44 | return 45 | } 46 | const service = new Service(filePath) 47 | return service.ctx.handlePreImport() 48 | } 49 | public isSupport() { 50 | return enableLang.includes(this.codeLang) 51 | } 52 | public execTest(testCase: TestCase) { 53 | return this.ctx.execTest(testCase) 54 | } 55 | public debugCodeCommand(folder: vscode.WorkspaceFolder, breaks: vscode.SourceBreakpoint[]) { 56 | return this.ctx.debugCodeCommand(folder, breaks) 57 | } 58 | public buildCode() { 59 | return this.ctx.buildCode() 60 | } 61 | public getTestCaseList(text: string) { 62 | return this.ctx.getTestCaseList(text) 63 | } 64 | public addComment(text, comment, funcName) { 65 | return this.ctx.addComment(text, comment, funcName) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/debugTask/index.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync, promises } from 'fs' 2 | import { outBoundArrayPlugin } from '../babelPlugin' 3 | import rollup = require('rollup') 4 | import resolve from '@rollup/plugin-node-resolve' 5 | import rollupBabelPlugin from '@rollup/plugin-babel' 6 | import * as path from 'path' 7 | import { CodeLang, getFileLang } from '../common/langConfig' 8 | import { DebugOptions } from '../common/lang' 9 | import { handleArgsType } from '../common/util' 10 | 11 | export async function main(options: DebugOptions, args: string[]) { 12 | const mainFilePath = options.filePath 13 | const dir = path.resolve(mainFilePath, '..', '..', '..') 14 | 15 | const outputDir = path.join(dir, 'out') 16 | const codeLang = getFileLang(mainFilePath) 17 | if (![CodeLang.JavaScript, CodeLang.TypeScript].includes(codeLang)) { 18 | console.log('only support JavaScript and TypeScript') 19 | return 20 | } 21 | await promises.mkdir(outputDir, { recursive: true }) 22 | const codePath = path.join(outputDir, 'code.js') 23 | const codeMapPath = path.join(outputDir, 'code.js.map') 24 | 25 | const mainFileCode = options.originCode 26 | 27 | const meta = options.metaData 28 | const funName = meta.name 29 | 30 | if (codeLang === CodeLang.TypeScript) { 31 | const finalCode = mainFileCode + '\n' + handleArgsType(meta, '', args, true) 32 | return buildTsCode(finalCode, mainFilePath, path.join(dir, 'out')) 33 | } 34 | 35 | const bundle = await rollup.rollup({ 36 | input: mainFilePath, 37 | treeshake: false, 38 | plugins: [ 39 | resolve(), 40 | rollupBabelPlugin({ 41 | babelHelpers: 'bundled', 42 | comments: false, 43 | plugins: [outBoundArrayPlugin], 44 | }), 45 | ], 46 | }) 47 | const { output } = await bundle.generate({ 48 | sourcemap: true, 49 | sourcemapPathTransform: (r, s) => { 50 | return path.join(path.parse(s).dir, r) 51 | }, 52 | }) 53 | let code = output[0].code 54 | const map = output[0].map 55 | if (funName) { 56 | code += handleArgsType(meta, '', args) + '\n' 57 | } 58 | 59 | code = code + '//# sourceMappingURL=code.js.map' 60 | if (map?.file) { 61 | map.file = 'code.js' 62 | } 63 | 64 | writeFileSync(codePath, code) 65 | writeFileSync(codeMapPath, JSON.stringify(map)) 66 | } 67 | 68 | export async function buildTsCode(text: string, filePath: string, dir: string) { 69 | // eslint-disable-next-line @typescript-eslint/no-var-requires 70 | return require('esbuild') 71 | .build({ 72 | stdin: { 73 | contents: text, 74 | loader: 'ts', 75 | resolveDir: dir, 76 | sourcefile: filePath, 77 | }, 78 | platform: 'node', 79 | mainFields: ['module', 'main'], 80 | bundle: true, 81 | format: 'esm', 82 | treeShaking: true, 83 | outfile: 'code.js', 84 | absWorkingDir: dir, 85 | sourcemap: 'inline', 86 | }) 87 | .catch((err) => { 88 | console.log(err) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/fileStorage/crc32_table.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * copy from https://docs.microsoft.com/en-us/openspecs/office_protocols/ms-abs/06966aa2-70da-4bf9-8448-3355f277cd77?redirectedfrom=MSDN 3 | */ 4 | 5 | const crc32_table: number[] = [ 6 | 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 7 | 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 8 | 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 9 | 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 10 | 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 11 | 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 12 | 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 13 | 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 14 | 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 15 | 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 16 | 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 17 | 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 18 | 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 19 | 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 20 | 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 21 | 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 22 | 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 23 | 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 24 | 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 25 | 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 26 | 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 27 | 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 28 | 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 29 | 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 30 | 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 31 | 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 32 | 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 33 | 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 34 | 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, 35 | ] 36 | export default crc32_table 37 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import { ConciseQuestion, MapIdConciseQuestion, Tag } from './model/common' 2 | import fs = require('fs') 3 | import { config, Config, log } from './config' 4 | import { reRequire } from './util' 5 | const COOKIE = 'cookie' 6 | const TAG = 'tag' 7 | export const ALLQUESTIONS = 'allQuestions' 8 | export const MAPIDQUESTION = 'MapIdQuestion' 9 | interface CacheMap { 10 | [COOKIE]: Cookie 11 | [TAG]: Tag[] 12 | [ALLQUESTIONS]: ConciseQuestion[] 13 | [MAPIDQUESTION]: MapIdConciseQuestion 14 | [titleSlug: string]: any 15 | } 16 | interface Cookie { 17 | cookie: string 18 | 'x-csrftoken': string 19 | } 20 | class Cache { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | private cacheMap: Partial = {} 23 | config: Config 24 | constructor() { 25 | this.config = config 26 | } 27 | removeCache() { 28 | this.cacheMap = {} 29 | } 30 | get(key: T) { 31 | return this.cacheMap[key] 32 | } 33 | set(key: T, value: R) { 34 | this.cacheMap[key] = value 35 | } 36 | has(key: string) { 37 | return key in this.cacheMap 38 | } 39 | async getCookie() { 40 | const { cookiePath } = config 41 | if (this.has(COOKIE)) { 42 | return this.get(COOKIE) 43 | } 44 | try { 45 | const cookie = reRequire(cookiePath) 46 | this.set(COOKIE, cookie) 47 | return this.get(COOKIE) 48 | } catch (err) { 49 | return null 50 | } 51 | } 52 | getQuestions(): ConciseQuestion[] { 53 | const { questionPath } = config 54 | if (this.has(ALLQUESTIONS)) { 55 | return this.get(ALLQUESTIONS) || [] 56 | } 57 | try { 58 | const allQuestions = reRequire(questionPath) 59 | let filterPrivilegeQuestions = allQuestions 60 | if (!config.displayLock) { 61 | filterPrivilegeQuestions = allQuestions.filter((v) => !v.paid_only) 62 | } 63 | this.set(ALLQUESTIONS, filterPrivilegeQuestions) 64 | return this.get(ALLQUESTIONS) || [] 65 | } catch (err) { 66 | return [] 67 | } 68 | } 69 | 70 | setQuestions(questions: ConciseQuestion[]) { 71 | const { questionPath } = config 72 | let filterPrivilegeQuestions = questions 73 | if (!config.displayLock) { 74 | filterPrivilegeQuestions = questions.filter((v) => !v.paid_only) 75 | } 76 | this.set(ALLQUESTIONS, filterPrivilegeQuestions) 77 | fs.writeFile( 78 | questionPath, 79 | JSON.stringify(filterPrivilegeQuestions), 80 | { 81 | encoding: 'utf8', 82 | }, 83 | (err) => { 84 | if (err) { 85 | log.append(err.message) 86 | } 87 | } 88 | ) 89 | } 90 | updateQuestion(id: number, params: Partial) { 91 | const questions: ConciseQuestion[] | undefined = this.get(ALLQUESTIONS) 92 | if (questions) { 93 | const question = questions.find((q) => q.id === id) 94 | if (question) { 95 | Object.assign(question, params) 96 | this.setQuestions(questions) 97 | } 98 | } 99 | } 100 | getTags() { 101 | const { tagPath } = config 102 | if (this.has(TAG)) { 103 | return this.get(TAG) 104 | } 105 | try { 106 | const tags = reRequire(tagPath) 107 | this.set(TAG, tags) 108 | return this.get(TAG) 109 | } catch (err) { 110 | return null 111 | } 112 | } 113 | setTags(tags: Tag[]) { 114 | const { tagPath } = config 115 | this.set(TAG, tags) 116 | fs.writeFile( 117 | tagPath, 118 | JSON.stringify(tags), 119 | { 120 | encoding: 'utf8', 121 | }, 122 | (err) => { 123 | if (err) { 124 | log.append(err.message) 125 | } 126 | } 127 | ) 128 | } 129 | } 130 | export const cache = new Cache() 131 | -------------------------------------------------------------------------------- /src/provider/questionsProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { QuestionPreview } from '../webview/questionPreview' 3 | import { config } from '../config' 4 | import { resolverEn, ResolverFn, ResolverParam, ResolverType } from './resolver' 5 | import { resolverCn } from './resolver.cn' 6 | import * as path from 'path' 7 | export class QuestionTree extends vscode.TreeItem { 8 | constructor( 9 | public readonly label: string, 10 | public readonly id: string, 11 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 12 | public readonly type: string, 13 | public readonly key?: string, 14 | public readonly param?: Partial, 15 | public readonly command?: vscode.Command 16 | ) { 17 | super(label, collapsibleState) 18 | } 19 | } 20 | export class QuestionsProvider implements vscode.TreeDataProvider { 21 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 22 | QuestionTree | undefined 23 | >() 24 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event 25 | constructor( 26 | private workspaceRoot: ReadonlyArray | undefined, 27 | private _extensionPath: string 28 | ) {} 29 | refresh(): void { 30 | this._onDidChangeTreeData.fire(undefined) 31 | } 32 | 33 | getTreeItem(element: QuestionTree): vscode.TreeItem { 34 | return element 35 | } 36 | getChildren(element?: QuestionTree): Thenable { 37 | let resolver: ResolverType 38 | if (config.lang === 'en') { 39 | resolver = resolverEn 40 | } else { 41 | resolver = resolverCn 42 | } 43 | if (element) { 44 | const type = element.type 45 | const key = element.key 46 | const param = element.param || {} 47 | let fn: ResolverFn 48 | if (key) { 49 | fn = resolver[type][key] 50 | } else { 51 | fn = resolver[type] as ResolverFn 52 | } 53 | return Promise.resolve(fn(param)) 54 | .then((arr) => { 55 | if (!arr.length) { 56 | return [] 57 | } 58 | const isLastChild = arr[0].isLast 59 | if (isLastChild) { 60 | return arr.map((v) => { 61 | const dep = new QuestionTree( 62 | v.label, 63 | v.id, 64 | vscode.TreeItemCollapsibleState.None, 65 | v.type, 66 | v.key, 67 | v.param, 68 | { 69 | title: 'QuestionPreview', 70 | command: QuestionPreview, 71 | arguments: [v.param], 72 | } 73 | ) 74 | if (v.paidOnly) { 75 | dep.iconPath = new vscode.ThemeIcon('lock') 76 | } else { 77 | // dep.iconPath = v.isAC ? new vscode.ThemeIcon('check') : '' 78 | dep.iconPath = v.isAC ? path.join(__dirname, '..', '..', 'media', 'ac.svg') : '' 79 | } 80 | dep.contextValue = 'memo' 81 | return dep 82 | }) 83 | } else { 84 | return arr.map( 85 | (v) => 86 | new QuestionTree( 87 | v.label, 88 | v.id, 89 | vscode.TreeItemCollapsibleState.Collapsed, 90 | v.type, 91 | v.key, 92 | v.param || {} 93 | ) 94 | ) 95 | } 96 | }) 97 | .catch((err) => { 98 | console.log(err) 99 | return [] 100 | }) 101 | } else { 102 | return Promise.resolve(resolver.Query()).then((arr) => 103 | arr.map( 104 | (v) => 105 | new QuestionTree( 106 | v.label, 107 | v.id, 108 | vscode.TreeItemCollapsibleState.Collapsed, 109 | v.type, 110 | v.key, 111 | v.param || {} 112 | ) 113 | ) 114 | ) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/suite/util.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | // import * as myExtension from '../../extension'; 6 | import { getTestCaseList, unionArr } from '../../common/util' 7 | import { addComment } from '../../common/transformCode' 8 | 9 | interface UnionArrDemo { 10 | params: T[][] 11 | result: T[] 12 | } 13 | const cases = [ 14 | `// @algorithm @lc id=1576 lang=javascript weekname=weekly-contest-191 15 | // @title reorder-routes-to-make-all-paths-lead-to-the-city-zero 16 | 17 | // @test(6,[[0,1],[1,3],[2,3],[4,0],[4,5]])=3 18 | // @test(5,[[1,0],[1,2],[3,2],[3,4]])=2 19 | // @test(3,[[1,0],[2,0]])=0 20 | /** 21 | * @param {number} n 22 | * @param {number[][]} connections 23 | * @return {number} 24 | */ 25 | var minReorder = function (n, connections) { 26 | } 27 | `, 28 | `// @algorithm 29 | 30 | //@test([1,2]) 31 | /** 32 | * @param {TreeNode} root 33 | * @return {void} 34 | */ 35 | var func = function(root) { 36 | } 37 | `, 38 | `// @algorithm 39 | 40 | // @test([4,2,7,1,3],2)=[2,1,3] 41 | /** 42 | * Definition for a binary tree node. 43 | * function TreeNode(val) { 44 | * this.val = val; 45 | * this.left = this.right = null; 46 | * } 47 | */ 48 | /** 49 | * @param {TreeNode} root 50 | * @param {number} val 51 | * @return {TreeNode} 52 | */ 53 | var searchBST = function (root, val) { 54 | };`, 55 | ] 56 | suite('Util Test Suite', () => { 57 | test('test getTestCaseList', () => { 58 | const testCaseList = getTestCaseList(cases[0]) 59 | assert.equal(testCaseList.length, 1) 60 | const { testCase, funcName, paramsTypes, resultType } = testCaseList[0] 61 | assert.deepEqual(testCase, [ 62 | '// @test(6,[[0,1],[1,3],[2,3],[4,0],[4,5]])=3', 63 | '// @test(5,[[1,0],[1,2],[3,2],[3,4]])=2', 64 | '// @test(3,[[1,0],[2,0]])=0', 65 | ]) 66 | assert.equal(funcName, 'minReorder') 67 | assert.deepEqual(paramsTypes, ['number', 'number[][]']) 68 | assert.equal(resultType, 'number') 69 | }) 70 | test('test void return', () => { 71 | const testCaseList = getTestCaseList(cases[1]) 72 | const { paramsTypes, resultType } = testCaseList[0] 73 | assert.equal(testCaseList.length, 1) 74 | assert.equal(resultType, 'void') 75 | assert.deepEqual(paramsTypes, ['TreeNode']) 76 | }) 77 | test('test ', () => { 78 | const testCaseList = getTestCaseList(cases[2]) 79 | assert.equal(testCaseList.length, 1) 80 | // assert.equal(resultType, 'void') 81 | // assert.deepEqual(paramsTypes, ['TreeNode']) 82 | }) 83 | test('test add comment', () => { 84 | const source = ` 85 | //this is a test 86 | /** 87 | * this is a test too 88 | */ 89 | function main() { 90 | 91 | } 92 | ` 93 | const result = ` 94 | //hhh 95 | //this is a test 96 | /** 97 | * this is a test too 98 | */ 99 | function main() { 100 | 101 | } 102 | ` 103 | const out = addComment(source, 'hhh', 'main') 104 | assert.deepStrictEqual(out, result) 105 | }) 106 | test('test unionArr', () => { 107 | const list: UnionArrDemo[] = [ 108 | { 109 | params: [ 110 | ['xiao', 'li'], 111 | ['li', 'wang'], 112 | ], 113 | result: ['xiao', 'li', 'wang'], 114 | }, 115 | { 116 | params: [['xiao', 'li'], []], 117 | result: ['xiao', 'li'], 118 | }, 119 | { 120 | params: [[], []], 121 | result: [], 122 | }, 123 | { 124 | params: [[], ['abc']], 125 | result: ['abc'], 126 | }, 127 | ] 128 | list.forEach(({ params, result }) => assert.deepStrictEqual(unionArr(params[0], params[1]), result)) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /src/history/storage.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { config } from '../config' 3 | import { pathExists, readJson, writeJson, ensureFile } from 'fs-extra' 4 | import { CheckResponse, Lang, UpdateCommentOption, UpdateRemoteCommentOption } from '../model/common' 5 | import { apiCn } from '../api' 6 | import { formatMemory, formatTimestamp } from '.' 7 | interface Param { 8 | filePath: string 9 | text: string 10 | result: CheckResponse 11 | desc?: string 12 | } 13 | interface LocalSubmit { 14 | id: string 15 | code: string 16 | submission: Submission 17 | result: CheckResponse 18 | } 19 | interface Submission { 20 | id: string 21 | statusDisplay: string 22 | lang: string 23 | runtime: string 24 | timestamp: string 25 | url: string 26 | isPending: string 27 | memory: string 28 | submissionComment: string 29 | } 30 | class SubmitStorage { 31 | static submitStorage = new SubmitStorage() 32 | arr: Param[] = [] 33 | isWork = false 34 | getFilePath(question_id: string) { 35 | const cacheDir = path.join(config.cacheDir, 'submit') 36 | return path.join(cacheDir, question_id + '.json') 37 | } 38 | async read(question_id: string): Promise { 39 | const filePath = this.getFilePath(question_id) 40 | try { 41 | const arr = await readJson(filePath) 42 | return arr 43 | } catch (err) { 44 | return [] 45 | } 46 | } 47 | async save(options: Param) { 48 | if (this.isWork) { 49 | this.arr.push(options) 50 | } else { 51 | this.isWork = true 52 | await this.innerSave(options) 53 | } 54 | } 55 | saveSubmit(options: Param) { 56 | if (options.desc) { 57 | this.updateRemoteComment({ 58 | id: options.result.submission_id, 59 | comment: options.desc, 60 | }) 61 | } 62 | 63 | this.save(options) 64 | } 65 | updateRemoteComment({ id, comment }: UpdateRemoteCommentOption) { 66 | if (config.lang === Lang.cn && comment) { 67 | return apiCn.api.updateComment({ 68 | submissionId: id, 69 | comment: comment, 70 | }) 71 | } 72 | } 73 | async updateComment({ id, questionId, comment }: UpdateCommentOption): Promise { 74 | const arr = await this.read(questionId) 75 | const item = arr.find((v) => v.id === id) 76 | if (item) { 77 | item.submission.submissionComment = comment 78 | } 79 | const filePath = this.getFilePath(questionId) 80 | return writeJson(filePath, arr) 81 | } 82 | private async innerSave(options: Param): Promise { 83 | try { 84 | const id = options.result.question_id as string 85 | const r = options.result 86 | const filePath = this.getFilePath(id) 87 | const obj: LocalSubmit = { 88 | id: r.submission_id, 89 | code: options.text, 90 | submission: { 91 | id: r.submission_id, 92 | isPending: 'Not Pending', 93 | submissionComment: options.desc || '', 94 | lang: r.lang, 95 | memory: formatMemory(r.memory), 96 | runtime: r.status_runtime, 97 | statusDisplay: r.status_msg, 98 | timestamp: formatTimestamp(r.task_finish_time), 99 | url: '/submissions/detail/' + r.submission_id + '/', 100 | }, 101 | result: options.result, 102 | } 103 | let arr: LocalSubmit[] = [] 104 | const exist = await pathExists(filePath) 105 | if (exist) { 106 | arr = await readJson(filePath) 107 | arr.push(obj) 108 | } else { 109 | arr.push(obj) 110 | } 111 | await ensureFile(filePath) 112 | await writeJson(filePath, arr) 113 | } catch (err) { 114 | console.log(err) 115 | } 116 | if (this.arr.length) { 117 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 118 | const opt = this.arr.shift()! 119 | await this.innerSave(opt) 120 | } else { 121 | this.isWork = false 122 | } 123 | } 124 | } 125 | 126 | export const submitStorage = SubmitStorage.submitStorage 127 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { config } from '../config' 2 | import * as apiCn from './api.cn' 3 | import * as apiEn from './api' 4 | import { getDb } from '../db' 5 | import * as modelCn from '../model/question.cn' 6 | import * as modelEn from '../model/question' 7 | 8 | export const api = { 9 | async fetchQuestionDetailById(id: string | number) { 10 | if (typeof id === 'string') { 11 | id = parseInt(id) 12 | } 13 | const db = await getDb() 14 | const out = await db.read(id) 15 | if (out) { 16 | return JSON.parse(out) 17 | } 18 | }, 19 | async fetchQuestionDetail(titleSlug: string) { 20 | if (config.lang === 'en') { 21 | return apiEn.api.fetchQuestionDetail(titleSlug) 22 | } else { 23 | return apiCn.api.fetchQuestionDetail(titleSlug) 24 | } 25 | }, 26 | async fetchContestQuestionDetail(titleSlug: string, weekname: string) { 27 | if (config.lang === 'en') { 28 | return apiEn.api.fetchContestQuestionDetail(titleSlug, weekname) 29 | } else { 30 | return apiCn.api.fetchContestQuestionDetail(titleSlug, weekname) 31 | } 32 | }, 33 | async fetchQuestionByItemId(id: string) { 34 | if (config.lang === 'en') { 35 | const res = await apiEn.api.fetchChapterItem(id) 36 | return res.item.question 37 | } 38 | }, 39 | async saveQuestionDetail(question) { 40 | const db = await getDb() 41 | await db.add(JSON.stringify(question), Number(question.questionId), Number(question.questionFrontendId)) 42 | }, 43 | async submitContest(options: modelCn.SubmitContestOptions | modelEn.SubmitContestOptions) { 44 | if (config.lang === 'en') { 45 | return apiEn.api.submitContest(options) 46 | } else { 47 | return apiCn.api.submitContest(options) 48 | } 49 | }, 50 | async submit(options: modelCn.SubmitOptions | modelEn.SubmitOptions) { 51 | if (config.lang === 'en') { 52 | return apiEn.api.submit(options) 53 | } else { 54 | return apiCn.api.submit(options) 55 | } 56 | }, 57 | 58 | async check(options: modelCn.CheckOptions | modelEn.CheckOptions) { 59 | if (config.lang === 'en') { 60 | return apiEn.api.check(options) 61 | } else { 62 | return apiCn.api.check(options) 63 | } 64 | }, 65 | checkContest(options: modelCn.CheckContestOptions | modelEn.CheckContestOptions) { 66 | if (config.lang === 'en') { 67 | return apiEn.api.checkContest(options) 68 | } else { 69 | return apiCn.api.checkContest(options) 70 | } 71 | }, 72 | async getAllQuestions() { 73 | if (config.lang === 'en') { 74 | return apiEn.api.getAllQuestions() 75 | } else { 76 | return apiCn.api.getAllQuestions() 77 | } 78 | }, 79 | async fetchSubmissions(options: modelCn.SubmissionsOptions | modelEn.SubmissionsOptions) { 80 | if (config.lang === 'en') { 81 | return apiEn.api.fetchSubmissions(options) 82 | } else { 83 | return apiCn.api.fetchSubmissions(options) 84 | } 85 | }, 86 | async fetchSubmissionDetail(options: modelCn.SubmissionDetailOptions) { 87 | if (config.lang === 'en') { 88 | return apiEn.api.fetchSubmissionDetail(options) 89 | } else { 90 | return apiCn.api.fetchSubmissionDetail(options) 91 | } 92 | }, 93 | } 94 | 95 | export async function refreshQuestions() { 96 | if (config.lang === 'en') { 97 | await apiEn.api.refreshQuestions() 98 | } else { 99 | await apiCn.api.refreshQuestions() 100 | } 101 | } 102 | 103 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 104 | function handleCategorieQuestions(data) { 105 | const { stat_status_pairs } = data 106 | const questions = stat_status_pairs.map((v) => ({ 107 | fid: String(v.stat.frontend_question_id), 108 | level: v.difficulty.level, 109 | id: v.stat.question_id, 110 | title: v.stat.question__title, 111 | slug: v.stat.question__title_slug, 112 | acs: v.stat.total_acs, 113 | submitted: v.stat.total_submitted, 114 | paid_only: v.paid_only, 115 | status: v.status, 116 | name: v.stat.question__title, 117 | })) 118 | return questions 119 | } 120 | export { apiEn, apiCn } 121 | -------------------------------------------------------------------------------- /src/history/answer.ts: -------------------------------------------------------------------------------- 1 | import { generateId, getDesc, getFuncNames, QuestionMeta, readFileAsync } from '../common/util' 2 | import { submitStorage } from './storage' 3 | import * as path from 'path' 4 | import { config } from '../config' 5 | import { fetchQuestion, getName } from '../webview/questionPreview' 6 | import { preprocessCode } from '../util' 7 | import { CodeLang, isAlgorithm, langMap } from '../common/langConfig' 8 | import { Service } from '../lang/common' 9 | import { writeFile } from '../common/util' 10 | import { readJson, writeJson, ensureFile } from 'fs-extra' 11 | import { UpdateCommentOption } from '../model/common' 12 | interface Answer { 13 | id: string 14 | code: string 15 | desc: string 16 | timestamp: string 17 | lang?: string 18 | titleSlug?: string 19 | questionId?: string 20 | } 21 | 22 | export class AnswerStorage { 23 | static answerStorage = new AnswerStorage() 24 | getFilePath(questionId: string) { 25 | const cacheDir = path.join(config.cacheDir, 'answer') 26 | return path.join(cacheDir, questionId + '.json') 27 | } 28 | static saveSubmit(filePath: string, text: string, result: any) { 29 | const desc = getDesc(text, filePath) || '' 30 | 31 | submitStorage.save({ 32 | filePath: filePath, 33 | text, 34 | result, 35 | desc, 36 | }) 37 | } 38 | async read(questionId: string): Promise { 39 | const filePath = this.getFilePath(questionId) 40 | try { 41 | const arr = await readJson(filePath) 42 | return arr 43 | } catch (err) { 44 | console.log(err) 45 | return [] 46 | } 47 | } 48 | async save(data: string, questionMeta: QuestionMeta) { 49 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 50 | const id = questionMeta.id! 51 | const historyfilePath = this.getFilePath(id) 52 | const arr = await this.read(id) 53 | const newAnswer: Answer = { 54 | code: data, 55 | id: generateId(), 56 | desc: questionMeta.desc || '', 57 | timestamp: Math.floor(Date.now() / 1000).toString(), 58 | lang: questionMeta.lang, 59 | questionId: id, 60 | titleSlug: questionMeta.titleSlug, 61 | } 62 | arr.push(newAnswer) 63 | await ensureFile(historyfilePath) 64 | await writeJson(historyfilePath, arr) 65 | } 66 | async updateComment({ id, questionId, comment }: UpdateCommentOption) { 67 | const arr = await this.read(questionId) 68 | const historyfilePath = this.getFilePath(questionId) 69 | const item = arr.find((v) => v.id === id) 70 | if (item) { 71 | item.desc = comment 72 | } 73 | await writeJson(historyfilePath, arr) 74 | } 75 | async newAnswer(filePath: string) { 76 | const data = await readFileAsync(filePath, { encoding: 'utf8' }) 77 | const { questionMeta } = getFuncNames(data, filePath) 78 | const id = questionMeta.id 79 | if (!id) { 80 | return 81 | } 82 | await this.save(data, questionMeta) 83 | 84 | const { weekname, lang: langSlug } = questionMeta 85 | const param = { 86 | titleSlug: questionMeta.titleSlug, 87 | weekname: questionMeta.weekname, 88 | questionId: questionMeta.id, 89 | } 90 | const question = await fetchQuestion(param) 91 | if (!question) { 92 | return 93 | } 94 | const { codeSnippets, questionFrontendId, title, translatedTitle } = question 95 | const codeSnippet = codeSnippets.find((codeSnippet) => codeSnippet.langSlug === langSlug) 96 | if (codeSnippet) { 97 | const langSlug = codeSnippet.langSlug 98 | const langConfig = langMap[langSlug] 99 | const { name } = getName(questionFrontendId, title, translatedTitle, codeSnippet) 100 | let code = preprocessCode(question, weekname, codeSnippet, name) 101 | if (langConfig.lang === CodeLang.Java) { 102 | code = code.replace('class Solution', 'public class Solution') 103 | } 104 | if (isAlgorithm(langConfig.lang)) { 105 | await Service.handlePreImport(filePath) 106 | } 107 | 108 | await writeFile(filePath, code) 109 | } 110 | } 111 | } 112 | export const answerStorage = AnswerStorage.answerStorage 113 | -------------------------------------------------------------------------------- /template/golang/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/franela/goblin" 7 | ) 8 | 9 | func Test(t *testing.T) { 10 | g := Goblin(t) 11 | g.Describe("test parse", func() { 12 | g.It("should parse integer", func() { 13 | a := parseInteger("12") 14 | g.Assert(a).Equal(12) 15 | }) 16 | g.It("should parse string", func() { 17 | a := parseString("\"12\"") 18 | g.Assert(a).Equal("12") 19 | }) 20 | g.It("should parse float", func() { 21 | a := parseFloat("1.22") 22 | g.Assert(a).Equal(1.22) 23 | }) 24 | g.It("should parse integer[]", func() { 25 | a := parseIntegerArr("[1,2,3]") 26 | g.Assert(a).Equal([]int{1, 2, 3}) 27 | }) 28 | g.It("should parse string[]", func() { 29 | a := parseStringArr("[\"1\",\"2\",\"3\"]") 30 | g.Assert(a).Equal([]string{"1", "2", "3"}) 31 | }) 32 | g.It("should parse integer[][]", func() { 33 | a := parseIntegerArrArr("[[1,2,3],[4,5,6]]") 34 | g.Assert(a).Equal([][]int{[]int{1, 2, 3}, []int{4, 5, 6}}) 35 | }) 36 | g.It("should parse string[][]", func() { 37 | a := parseStringArrArr("[[\"1\",\"2\",\"3\"],[\"4\",\"5\",\"6\"]]") 38 | g.Assert(a).Equal([][]string{[]string{"1", "2", "3"}, []string{"4", "5", "6"}}) 39 | }) 40 | }) 41 | g.Describe("test serialize", func() { 42 | g.It("should serialize integer", func() { 43 | a := serializeInterface(12) 44 | g.Assert(a).Equal("12") 45 | }) 46 | g.It("should serialize string", func() { 47 | a := serializeInterface("12") 48 | g.Assert(a).Equal("\"12\"") 49 | }) 50 | g.It("should serialize double", func() { 51 | a := serializeFloat(1.22) 52 | g.Assert(a).Equal("1.22000") 53 | }) 54 | g.It("should serialize integer[]", func() { 55 | a := serializeInterface([]int{1, 2, 3}) 56 | g.Assert(a).Equal("[1,2,3]") 57 | }) 58 | g.It("should serialize string[]", func() { 59 | a := serializeInterface([]string{"1", "2", "3"}) 60 | g.Assert(a).Equal("[\"1\",\"2\",\"3\"]") 61 | }) 62 | g.It("should serialize integer[][]", func() { 63 | a := serializeInterface([][]int{[]int{1, 2, 3}, []int{4, 5, 6}}) 64 | g.Assert(a).Equal("[[1,2,3],[4,5,6]]") 65 | }) 66 | g.It("should serialize string[][]", func() { 67 | a := serializeInterface([][]string{[]string{"1", "2", "3"}, []string{"4", "5", "6"}}) 68 | g.Assert(a).Equal("[[\"1\",\"2\",\"3\"],[\"4\",\"5\",\"6\"]]") 69 | }) 70 | g.It("should serialize list", func() { 71 | a := serializeInterface([]int{1, 2, 3}) 72 | g.Assert(a).Equal("[1,2,3]") 73 | }) 74 | 75 | g.It("should serialize list", func() { 76 | a := serializeInterface([]string{"1", "2", "3"}) 77 | g.Assert(a).Equal("[\"1\",\"2\",\"3\"]") 78 | }) 79 | g.It("should serialize list>", func() { 80 | a := serializeInterface([][]int{[]int{1, 2, 3}, []int{4, 5, 6}}) 81 | g.Assert(a).Equal("[[1,2,3],[4,5,6]]") 82 | }) 83 | g.It("should serialize list>", func() { 84 | a := serializeInterface([][]string{[]string{"1", "2", "3"}, []string{"4", "5", "6"}}) 85 | g.Assert(a).Equal("[[\"1\",\"2\",\"3\"],[\"4\",\"5\",\"6\"]]") 86 | }) 87 | g.It("should serialize TreeNode", func() { 88 | treeNode := deserializeTreeNode("[1,2,3,null,null,4,5]") 89 | str := serializeTreeNode(treeNode) 90 | g.Assert(str).Equal("[1,2,3,null,null,4,5]") 91 | 92 | }) 93 | g.It("should serialize ListNode", func() { 94 | treeNode := deserializeListNode("[1,2,3,4,5]") 95 | str := serializeListNode(treeNode) 96 | g.Assert(str).Equal("[1,2,3,4,5]") 97 | }) 98 | g.It("should serialize list", func() { 99 | data := "[[1,2,3,null,null,4,5],[1,2,3]]" 100 | t1 := deserializeTreeNodeArr(data) 101 | str := serializeTreeNodeArr(t1) 102 | g.Assert(str).Equal(data) 103 | }) 104 | g.It("should serialize list", func() { 105 | data := "[[4,5,6],[1,2,3]]" 106 | t1 := deserializeListNodeArr(data) 107 | str := serializeListNodeArr(t1) 108 | g.Assert(str).Equal(data) 109 | }) 110 | }) 111 | } 112 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { languages, commands } from 'vscode' 3 | import { CodelensProvider } from './provider/codelensProvider' 4 | import { 5 | testCodeCommand, 6 | buildCodeCommand, 7 | submitCommand, 8 | refreshCommand, 9 | switchEndpointCommand, 10 | signInCommand, 11 | switchCodeLangCommand, 12 | debugCodeCommand, 13 | getDescriptionCommand, 14 | searchCommand, 15 | memoFilePreviewCommand, 16 | addFolderCommand, 17 | addMemoFileCommand, 18 | removeMemoFileCommand, 19 | switchDataBaseCommand, 20 | viewSubmitHistoryCommand, 21 | newAnswerCommand, 22 | } from './commands' 23 | import { QuestionsProvider } from './provider/questionsProvider' 24 | import { createQuestionPanelCommand } from './webview/questionPreview' 25 | import { config, onChangeConfig } from './config' 26 | import { registerForSnippetProviders } from './provider/snippetProvider' 27 | import { registerDebug } from './debug/launch' 28 | import { registerCompletionItemProvider } from './provider/completionProvider' 29 | import { MemoProvider } from './provider/memoProvider' 30 | import { childProcessProxy } from './process' 31 | export function activate(context: vscode.ExtensionContext) { 32 | const subscriptions = context.subscriptions 33 | registerCompletionItemProvider(context) 34 | registerForSnippetProviders(context) 35 | registerDebug() 36 | const codelensProvider = new CodelensProvider() 37 | languages.registerCodeLensProvider('*', codelensProvider) 38 | const questionsProvider = new QuestionsProvider(vscode.workspace.workspaceFolders, context.extensionPath) 39 | config.questionsProvider = questionsProvider 40 | const memoProvider = new MemoProvider() 41 | subscriptions.push(commands.registerCommand('algorithm.testCode', testCodeCommand)) 42 | subscriptions.push(commands.registerCommand('algorithm.debugCode', debugCodeCommand)) 43 | subscriptions.push(commands.registerCommand('algorithm.buildCode', buildCodeCommand.bind(null, context))) 44 | subscriptions.push(commands.registerCommand('algorithm.submit', submitCommand.bind(null, questionsProvider))) 45 | subscriptions.push( 46 | commands.registerCommand('algorithm.getDescription', getDescriptionCommand.bind(null, context.extensionPath)) 47 | ) 48 | subscriptions.push( 49 | commands.registerCommand( 50 | 'algorithm.questionPreview', 51 | createQuestionPanelCommand.bind(null, context.extensionPath) 52 | ) 53 | ) 54 | subscriptions.push( 55 | commands.registerCommand('algorithm.refreshQuestions', refreshCommand.bind(null, questionsProvider)) 56 | ) 57 | subscriptions.push( 58 | commands.registerCommand('algorithm.switchEndpoint', switchEndpointCommand.bind(null, questionsProvider)) 59 | ) 60 | subscriptions.push(commands.registerCommand('algorithm.signIn', signInCommand.bind(null, questionsProvider))) 61 | subscriptions.push( 62 | commands.registerCommand('algorithm.switchCodeLang', switchCodeLangCommand.bind(null, questionsProvider)) 63 | ) 64 | subscriptions.push(commands.registerCommand('algorithm.search', searchCommand)) 65 | subscriptions.push(commands.registerCommand('algorithm.memoFilePreview', memoFilePreviewCommand)) 66 | subscriptions.push(commands.registerCommand('algorithm.addFolder', addFolderCommand.bind(null, memoProvider))) 67 | subscriptions.push(commands.registerCommand('algorithm.addMemoFile', addMemoFileCommand.bind(null, memoProvider))) 68 | subscriptions.push( 69 | commands.registerCommand('algorithm.removeMemoFile', removeMemoFileCommand.bind(null, memoProvider)) 70 | ) 71 | subscriptions.push(commands.registerCommand('algorithm.switchDataBase', switchDataBaseCommand)) 72 | subscriptions.push( 73 | commands.registerCommand('algorithm.viewSubmitHistory', viewSubmitHistoryCommand.bind(null, context)) 74 | ) 75 | subscriptions.push(commands.registerCommand('algorithm.newAnswer', newAnswerCommand)) 76 | 77 | vscode.window.createTreeView('questions', { 78 | treeDataProvider: questionsProvider, 79 | showCollapseAll: true, 80 | }) 81 | vscode.window.createTreeView('memo', { 82 | treeDataProvider: memoProvider, 83 | showCollapseAll: true, 84 | }) 85 | subscriptions.push(vscode.workspace.onDidChangeConfiguration(onChangeConfig.bind(null, questionsProvider))) 86 | } 87 | 88 | export function deactivate() { 89 | childProcessProxy.clear() 90 | } -------------------------------------------------------------------------------- /src/provider/resolver.cn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | api, 3 | getTags, 4 | getQuestionsByTag, 5 | getQuestionsByDifficult, 6 | getAllQuestions, 7 | getCategories, 8 | getQuestionsByCategory, 9 | } from '../api/api.cn' 10 | import { ResolverType } from './resolver' 11 | import { normalizeQuestions } from '../common/util' 12 | 13 | export const resolverCn: ResolverType = { 14 | Query() { 15 | //'Company', 'Favorite', 16 | return ['All', 'Difficulty', 'Category', 'Tag', 'Contest', 'TodayRecord'].map((v) => ({ 17 | key: v.toLowerCase(), 18 | type: 'Catalogue', 19 | label: v, 20 | id: v, 21 | })) 22 | }, 23 | Catalogue: { 24 | async all() { 25 | const questions = await getAllQuestions() 26 | return normalizeQuestions(questions, 'all') 27 | }, 28 | difficulty() { 29 | return ['Easy', 'Medium', 'Hard'].map((v) => ({ 30 | key: v.toLowerCase(), 31 | type: 'Difficulty', 32 | label: v, 33 | id: v, 34 | })) 35 | }, 36 | category() { 37 | const categories = getCategories() 38 | return categories.map((v) => ({ 39 | label: v.label, 40 | type: 'Category', 41 | key: v.category_slug, 42 | id: 'Category' + v.category_slug, 43 | })) 44 | }, 45 | async tag() { 46 | const tags = await getTags() 47 | return tags.map((tag) => ({ 48 | key: 'tagkey', 49 | type: 'Tag', 50 | label: tag, 51 | id: tag, 52 | param: { 53 | tag, 54 | }, 55 | })) 56 | }, 57 | 58 | // company() { 59 | 60 | // }, 61 | // favorite() { 62 | 63 | // }, 64 | async contest() { 65 | const contests = await api.fetchContests() 66 | return contests.map((contest) => ({ 67 | key: 'contestKey', 68 | type: 'Contest', 69 | label: contest.title, 70 | id: contest.title, 71 | param: { 72 | titleSlug: contest.titleSlug, 73 | }, 74 | })) 75 | }, 76 | async todayrecord() { 77 | const record = await api.fetchTodayRecord() 78 | return record.map((v) => ({ 79 | type: 'Question', 80 | label: v.question.questionFrontendId + '.' + v.question.translatedTitle, 81 | isAC: v.userStatus === 'FINISH', //NOT_START 82 | isLast: true, 83 | id: 'todayrecord' + v.question.questionFrontendId, 84 | param: { 85 | titleSlug: v.question.titleSlug, 86 | }, 87 | })) 88 | }, 89 | }, 90 | Category: { 91 | async algorithm() { 92 | const key = 'algorithm' 93 | const questions = await getQuestionsByCategory(key) 94 | return normalizeQuestions(questions, key) 95 | }, 96 | async lcci() { 97 | const key = 'lcci' 98 | const questions = await getQuestionsByCategory(key) 99 | return normalizeQuestions(questions, key) 100 | }, 101 | async lcof() { 102 | const key = 'lcof' 103 | const questions = await getQuestionsByCategory(key) 104 | return normalizeQuestions(questions, key) 105 | }, 106 | }, 107 | Tag: { 108 | async tagkey({ tag }) { 109 | if (!tag) { 110 | return [] 111 | } 112 | const questions = await getQuestionsByTag(tag) 113 | return normalizeQuestions(questions, 'tag' + tag) 114 | }, 115 | }, 116 | Difficulty: { 117 | async easy() { 118 | const key = 'easy' 119 | const questions = await getQuestionsByDifficult(key) 120 | return normalizeQuestions(questions, key) 121 | }, 122 | async medium() { 123 | const key = 'medium' 124 | const questions = await getQuestionsByDifficult(key) 125 | return normalizeQuestions(questions, key) 126 | }, 127 | async hard() { 128 | const key = 'hard' 129 | const questions = await getQuestionsByDifficult(key) 130 | return normalizeQuestions(questions, key) 131 | }, 132 | }, 133 | // Company() { 134 | 135 | // }, 136 | // Favorite() { 137 | 138 | // }, 139 | Contest: { 140 | async contestKey({ titleSlug }) { 141 | if (!titleSlug) { 142 | return [] 143 | } 144 | const data = await api.fetchContest(titleSlug) 145 | const questions = data.questions 146 | return questions.map((question) => ({ 147 | type: 'QuestionContest', 148 | label: question.title, 149 | id: 'QuestionContest' + question.id, 150 | isLast: true, 151 | param: { 152 | titleSlug: question.title_slug, 153 | weekname: titleSlug, 154 | }, 155 | })) 156 | }, 157 | }, 158 | } 159 | -------------------------------------------------------------------------------- /media/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs2015.css 3 | */ 4 | /* 5 | * Visual Studio 2015 dark style 6 | * Author: Nicolas LLOBERA 7 | */ 8 | 9 | 10 | .hljs-keyword, 11 | .hljs-literal, 12 | .hljs-symbol, 13 | .hljs-name { 14 | color: #569CD6; 15 | } 16 | .hljs-link { 17 | color: #569CD6; 18 | text-decoration: underline; 19 | } 20 | 21 | .hljs-built_in, 22 | .hljs-type { 23 | color: #4EC9B0; 24 | } 25 | 26 | .hljs-number, 27 | .hljs-class { 28 | color: #B8D7A3; 29 | } 30 | 31 | .hljs-string, 32 | .hljs-meta-string { 33 | color: #D69D85; 34 | } 35 | 36 | .hljs-regexp, 37 | .hljs-template-tag { 38 | color: #9A5334; 39 | } 40 | 41 | .hljs-subst, 42 | .hljs-function, 43 | .hljs-title, 44 | .hljs-params, 45 | .hljs-formula { 46 | color: #DCDCDC; 47 | } 48 | 49 | .hljs-comment, 50 | .hljs-quote { 51 | color: #57A64A; 52 | font-style: italic; 53 | } 54 | 55 | .hljs-doctag { 56 | color: #608B4E; 57 | } 58 | 59 | .hljs-meta, 60 | .hljs-meta-keyword, 61 | .hljs-tag { 62 | color: #9B9B9B; 63 | } 64 | 65 | .hljs-variable, 66 | .hljs-template-variable { 67 | color: #BD63C5; 68 | } 69 | 70 | .hljs-attr, 71 | .hljs-attribute, 72 | .hljs-builtin-name { 73 | color: #9CDCFE; 74 | } 75 | 76 | .hljs-section { 77 | color: gold; 78 | } 79 | 80 | .hljs-emphasis { 81 | font-style: italic; 82 | } 83 | 84 | .hljs-strong { 85 | font-weight: bold; 86 | } 87 | 88 | /*.hljs-code { 89 | font-family:'Monospace'; 90 | }*/ 91 | 92 | .hljs-bullet, 93 | .hljs-selector-tag, 94 | .hljs-selector-id, 95 | .hljs-selector-class, 96 | .hljs-selector-attr, 97 | .hljs-selector-pseudo { 98 | color: #D7BA7D; 99 | } 100 | 101 | .hljs-addition { 102 | background-color: var(--vscode-diffEditor-insertedTextBackground, rgba(155, 185, 85, 0.2)); 103 | color: rgb(155, 185, 85); 104 | display: inline-block; 105 | width: 100%; 106 | } 107 | 108 | .hljs-deletion { 109 | background: var(--vscode-diffEditor-removedTextBackground, rgba(255, 0, 0, 0.2)); 110 | color: rgb(255, 0, 0); 111 | display: inline-block; 112 | width: 100%; 113 | } 114 | 115 | 116 | /* 117 | From https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs.css 118 | */ 119 | /* 120 | 121 | Visual Studio-like style based on original C# coloring by Jason Diamond 122 | 123 | */ 124 | 125 | .vscode-light .hljs-function, 126 | .vscode-light .hljs-params, 127 | .vscode-light .hljs-number, 128 | .vscode-light .hljs-class { 129 | color: inherit; 130 | } 131 | 132 | .vscode-light .hljs-comment, 133 | .vscode-light .hljs-quote, 134 | .vscode-light .hljs-number, 135 | .vscode-light .hljs-class, 136 | .vscode-light .hljs-variable { 137 | color: #008000; 138 | } 139 | 140 | .vscode-light .hljs-keyword, 141 | .vscode-light .hljs-selector-tag, 142 | .vscode-light .hljs-name, 143 | .vscode-light .hljs-tag { 144 | color: #00f; 145 | } 146 | 147 | .vscode-light .hljs-built_in, 148 | .vscode-light .hljs-builtin-name { 149 | color: #007acc; 150 | } 151 | 152 | .vscode-light .hljs-string, 153 | .vscode-light .hljs-section, 154 | .vscode-light .hljs-attribute, 155 | .vscode-light .hljs-literal, 156 | .vscode-light .hljs-template-tag, 157 | .vscode-light .hljs-template-variable, 158 | .vscode-light .hljs-type { 159 | color: #a31515; 160 | } 161 | 162 | .vscode-light .hljs-selector-attr, 163 | .vscode-light .hljs-selector-pseudo, 164 | .vscode-light .hljs-meta, 165 | .vscode-light .hljs-meta-keyword { 166 | color: #2b91af; 167 | } 168 | 169 | .vscode-light .hljs-title, 170 | .vscode-light .hljs-doctag { 171 | color: #808080; 172 | } 173 | 174 | .vscode-light .hljs-attr { 175 | color: #f00; 176 | } 177 | 178 | .vscode-light .hljs-symbol, 179 | .vscode-light .hljs-bullet, 180 | .vscode-light .hljs-link { 181 | color: #00b0e8; 182 | } 183 | 184 | 185 | .vscode-light .hljs-emphasis { 186 | font-style: italic; 187 | } 188 | 189 | .vscode-light .hljs-strong { 190 | font-weight: bold; 191 | } -------------------------------------------------------------------------------- /src/babelPlugin.ts: -------------------------------------------------------------------------------- 1 | import { types as t } from '@babel/core' 2 | const regexp = /^@get\((-?\d+|Infinity|-Infinity)\)$/ 3 | 4 | function generatelogicalExpression(arr: t.Expression[]) { 5 | if (arr.length === 2) { 6 | return t.logicalExpression('&&', arr[0], arr[1]) 7 | } else if (arr.length > 2) { 8 | return t.logicalExpression('&&', arr[0], generatelogicalExpression(arr.slice(1))) 9 | } else { 10 | return arr[0] 11 | } 12 | } 13 | 14 | function toBinaryExpression(node) { 15 | return t.binaryExpression('>=', node, t.numericLiteral(0)) 16 | } 17 | type NumberProperty = t.Identifier | t.NumericLiteral | t.BinaryExpression 18 | type ComputeProperty = t.Identifier | t.BinaryExpression 19 | function isValidNumberProperty(property: t.Expression | t.Identifier | t.PrivateName): property is NumberProperty { 20 | return property.type === 'Identifier' || property.type === 'NumericLiteral' || property.type === 'BinaryExpression' 21 | } 22 | function isValidComputeProperty(property: NumberProperty): property is ComputeProperty { 23 | return property.type === 'Identifier' || property.type === 'BinaryExpression' 24 | } 25 | export function outBoundArrayPlugin() { 26 | return { 27 | visitor: { 28 | VariableDeclaration(path: babel.NodePath) { 29 | if ( 30 | t.isVariableDeclaration(path.node) && 31 | path.node.leadingComments && 32 | path.node.leadingComments.find((v) => regexp.test(v.value)) 33 | ) { 34 | const comment = path.node.leadingComments.find((v) => regexp.test(v.value)) as t.Comment 35 | const regexpResult = regexp.exec(comment.value) as RegExpExecArray 36 | const numStr = regexpResult[1] 37 | let numNode: t.Identifier | t.UnaryExpression | t.NumericLiteral 38 | if (numStr === 'Infinity') { 39 | numNode = t.identifier('Infinity') 40 | } else if (numStr === '-Infinity') { 41 | numNode = t.unaryExpression('-', t.identifier('Infinity')) 42 | } else { 43 | numNode = t.numericLiteral(parseInt(numStr)) 44 | } 45 | const declaration = path.node.declarations[0] 46 | const id = declaration.id 47 | if (!t.isIdentifier(id)) { 48 | return 49 | } 50 | const name = id.name 51 | const bind = path.scope.bindings[name] 52 | const referencePaths = bind.referencePaths 53 | referencePaths.forEach((r) => { 54 | let nodes: ComputeProperty[] = [] 55 | while (r.parentPath && r.parentPath.node.type === 'MemberExpression' && r.parentPath.node.computed) { 56 | const node = r.parentPath.node 57 | if (!isValidNumberProperty(node.property)) { 58 | return 59 | } 60 | if (isValidComputeProperty(node.property)) { 61 | nodes.push(node.property) 62 | } 63 | 64 | r = r.parentPath 65 | } 66 | 67 | if (nodes.length && !(r.key === 'left' && r.parentPath && r.parentPath.type === 'AssignmentExpression')) { 68 | nodes = nodes.map((node) => toBinaryExpression(node)) 69 | r.replaceWith( 70 | t.conditionalExpression( 71 | generatelogicalExpression(nodes), 72 | r.node as t.MemberExpression, 73 | numNode 74 | ) 75 | ) 76 | } 77 | }) 78 | } 79 | }, 80 | }, 81 | } 82 | } 83 | 84 | export function generateAddTestCommentPlugin(funcName: string, comment: string) { 85 | return function addTestCommentPlugin() { 86 | return { 87 | visitor: { 88 | VariableDeclaration(path: babel.NodePath) { 89 | const funcDeclaration = path.node.declarations.find((dec) => { 90 | return ( 91 | dec.id.type === 'Identifier' && 92 | dec.id.name === funcName && 93 | dec.init?.type === 'FunctionExpression' 94 | ) 95 | }) 96 | if (funcDeclaration) { 97 | path.addComment('leading', comment, true) 98 | path.stop() 99 | } 100 | }, 101 | FunctionDeclaration(path: babel.NodePath) { 102 | const name = path.node.id?.name 103 | if (name === funcName) { 104 | path.addComment('leading', comment, true) 105 | path.stop() 106 | } 107 | }, 108 | }, 109 | } 110 | } 111 | } 112 | 113 | export function removeExtraTypePlugin() { 114 | return { 115 | visitor: { 116 | ClassDeclaration(path: babel.NodePath) { 117 | const extraTypes = ['ListNode', 'TreeNode'] 118 | if (path.node.id && extraTypes.includes(path.node.id.name)) { 119 | path.remove() 120 | } 121 | }, 122 | }, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/debug/launch.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | import { Breakpoint, debug, SourceBreakpoint } from 'vscode' 3 | import { config } from '../config' 4 | import * as vscode from 'vscode' 5 | import { WorkspaceFolder, DebugConfiguration, ProviderResult, CancellationToken } from 'vscode' 6 | import * as path from 'path' 7 | import { getDebugConfig } from '../util' 8 | import { writeFileSync } from '../common/util' 9 | // import { Map } from '../common/map' 10 | interface Map { 11 | has( 12 | this: MapWith, 13 | key: CheckedString 14 | ): this is MapWith 15 | 16 | has(this: Map, key: CheckedString): this is MapWith 17 | } 18 | 19 | interface MapWith extends Map { 20 | get(k: DefiniteKey): V 21 | get(k: K): V | undefined 22 | } 23 | export function registerDebug() { 24 | let breaks: Breakpoint[] = [] 25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 26 | class ConfigurationProvider implements vscode.DebugConfigurationProvider { 27 | provideDebugConfigurations( 28 | _folder: WorkspaceFolder | undefined, 29 | _token?: CancellationToken 30 | ): ProviderResult { 31 | const debugConfiguration = getDebugConfig() 32 | return [debugConfiguration] 33 | } 34 | /** 35 | * Massage a debug configuration just before a debug session is being launched, 36 | * e.g. add all missing attributes to the debug configuration. 37 | */ 38 | resolveDebugConfigurationWithSubstitutedVariables( 39 | _folder: WorkspaceFolder | undefined, 40 | debugConfig: DebugConfiguration, 41 | _token?: CancellationToken 42 | ): ProviderResult { 43 | const { nodeBinPath } = config 44 | debugConfig.runtimeExecutable = nodeBinPath 45 | return debugConfig 46 | } 47 | } 48 | class TaskProvider implements vscode.TaskProvider { 49 | static TaskType = 'algorithm' 50 | provideTasks() { 51 | const { debugOptionsFilePath, nodeBinPath } = config 52 | const debugTaskFilePath = path.resolve(__dirname, '../debugTask', 'index.js') 53 | const taskName = 'build' 54 | const tasks = [ 55 | new vscode.Task( 56 | { 57 | type: TaskProvider.TaskType, 58 | }, 59 | vscode.TaskScope.Workspace, 60 | taskName, 61 | TaskProvider.TaskType, 62 | new vscode.ProcessExecution(nodeBinPath, [debugTaskFilePath, '${file}', debugOptionsFilePath]) 63 | ), 64 | ] 65 | const param = serializeBreaks(breaks as SourceBreakpoint[]) 66 | writeFileSync(debugOptionsFilePath, param, { encoding: 'utf8' }) 67 | return tasks 68 | } 69 | resolveTask(task: vscode.Task, _token?: CancellationToken | undefined): ProviderResult { 70 | return task 71 | } 72 | } 73 | 74 | debug.onDidChangeBreakpoints((e) => { 75 | breaks = breaks.concat(e.added) 76 | const removePoints = e.removed 77 | const editPoints = e.changed 78 | breaks = breaks.filter((b) => !removePoints.find((v) => v.id === b.id)) 79 | breaks = breaks.map((v) => editPoints.find((eb) => eb.id === v.id) || v) 80 | }) 81 | vscode.tasks.registerTaskProvider('algorithm', new TaskProvider()) 82 | 83 | // Automatically set configuration more better 84 | // debug.registerDebugConfigurationProvider('node', new ConfigurationProvider()) 85 | } 86 | 87 | interface CustomBreakpoint { 88 | path: string 89 | lines: number[] 90 | } 91 | function serializeBreaks(breaks: SourceBreakpoint[]): string { 92 | const pathMap = new Map() 93 | breaks.forEach((b) => { 94 | const p = b.location.uri.fsPath 95 | const line = b.location.range.start.line 96 | if (!pathMap.has(p)) { 97 | pathMap.set(p, [line]) 98 | } else { 99 | pathMap.get(p)!.push(line) 100 | } 101 | }) 102 | const r: CustomBreakpoint[] = [] 103 | for (const key of pathMap.keys()) { 104 | r.push({ path: key, lines: pathMap.get(key)! }) 105 | } 106 | return JSON.stringify(r) 107 | } 108 | export function tranfromToCustomBreakpoint(breaks: SourceBreakpoint[]) { 109 | const pathMap = new Map() 110 | breaks.forEach((b) => { 111 | const p = b.location.uri.fsPath 112 | const line = b.location.range.start.line 113 | 114 | if (!pathMap.has(p)) { 115 | pathMap.set(p, [line]) 116 | } else { 117 | pathMap.get(p)!.push(line) 118 | } 119 | }) 120 | const r: CustomBreakpoint[] = [] 121 | for (const key of pathMap.keys()) { 122 | r.push({ path: key, lines: pathMap.get(key)! }) 123 | } 124 | return r 125 | } 126 | -------------------------------------------------------------------------------- /src/child_process/execTestCode.ts: -------------------------------------------------------------------------------- 1 | import rollup = require('rollup') 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import rollupBabelPlugin from '@rollup/plugin-babel' 4 | import { transformSync } from '@babel/core' 5 | import * as path from 'path' 6 | import * as fs from 'fs' 7 | import sourceMap = require('source-map') 8 | import { TestResult, handleArgsType } from '../common/util' 9 | import { outBoundArrayPlugin } from '../babelPlugin' 10 | import { Script } from 'vm' 11 | import { handleMsg } from '../common/util' 12 | import { CodeLang, getFileLang } from '../common/langConfig' 13 | import presetTs = require('@babel/preset-typescript') 14 | import { TestOptions } from '../common/lang' 15 | const virtual = require('@rollup/plugin-virtual') 16 | 17 | const defaultTimeout = 10000 18 | const supportCodeLang = [CodeLang.JavaScript, CodeLang.TypeScript] 19 | let options = '' 20 | process.stdin.on('data', (data) => { 21 | options += data 22 | }) 23 | process.stdin.on('end', async () => { 24 | try { 25 | const msg = await execTestCase(JSON.parse(options.toString())) 26 | console.log(msg) 27 | } catch (err) { 28 | console.log(err) 29 | } 30 | }) 31 | async function execTestCase(options: TestOptions) { 32 | const { caseList, filePath, metaData } = options 33 | const lang = getFileLang(filePath) 34 | if (!supportCodeLang.includes(lang)) { 35 | return `${lang} is currently not supported` 36 | } 37 | const output = await buildCode(filePath, lang) 38 | const code = output[0].code 39 | const list: TestResult[] = [] 40 | for (const { args, result: expect } of caseList) { 41 | const originArgs = [...args] 42 | 43 | const finalCode = handleArgsType(metaData, code, args) 44 | const script = new Script(finalCode, {}) 45 | try { 46 | const result = script.runInNewContext( 47 | { 48 | console, 49 | }, 50 | { 51 | timeout: defaultTimeout, 52 | displayErrors: true, 53 | lineOffset: 0, 54 | } 55 | ) 56 | list.push({ 57 | args: originArgs.join(','), 58 | expect: expect, 59 | result: result, 60 | }) 61 | } catch (err) { 62 | return handleErrPosition(err, output[0].map as rollup.SourceMap, originArgs) 63 | } 64 | } 65 | return handleMsg(list) 66 | } 67 | 68 | async function buildCode(filePath: string, lang: CodeLang) { 69 | if (lang === CodeLang.TypeScript) { 70 | return buildTsCode(filePath) 71 | } 72 | return buildJsCode(filePath) 73 | } 74 | async function buildJsCode(filePath: string) { 75 | const plugins = [ 76 | resolve(), 77 | rollupBabelPlugin({ 78 | babelHelpers: 'bundled', 79 | comments: false, 80 | plugins: [outBoundArrayPlugin], 81 | }), 82 | ] 83 | const bundle = await rollup.rollup({ 84 | input: filePath, 85 | treeshake: false, 86 | plugins, 87 | }) 88 | const { output } = await bundle.generate({ 89 | sourcemap: true, 90 | sourcemapPathTransform: (r, s) => { 91 | return path.join(path.parse(s).dir, r) 92 | }, 93 | }) 94 | return output 95 | } 96 | 97 | async function buildTsCode(filePath: string) { 98 | const code = fs.readFileSync(filePath, { encoding: 'utf8' }) 99 | const fileDir = path.parse(filePath).dir 100 | const entry = transformSync(code, { 101 | filename: filePath, 102 | comments: false, 103 | cwd: fileDir, 104 | presets: [[presetTs, { onlyRemoveTypeImports: true }]], 105 | plugins: [outBoundArrayPlugin], 106 | }) 107 | 108 | const bundle = await rollup.rollup({ 109 | input: 'entry', 110 | treeshake: false, 111 | plugins: [ 112 | virtual({ 113 | entry: entry?.code || "", 114 | }), 115 | resolve({ rootDir: fileDir, browser: false }), 116 | ], 117 | }) 118 | const { output } = await bundle.generate({}) 119 | return output 120 | } 121 | 122 | async function handleErrPosition(err: Error, map: sourceMap.RawSourceMap, args: string[]) { 123 | const consumer = await new sourceMap.SourceMapConsumer(map) 124 | 125 | const regexp = /evalmachine\.:(\d+):?(\d+)?/g 126 | let msg = `× @test(${args.join(',')})\n` 127 | const stack: string = err.stack as string 128 | msg += stack.replace(regexp, (_, line, column) => { 129 | line = parseInt(line) 130 | column = parseInt(column) || 0 131 | const originPosition = consumer.originalPositionFor({ 132 | line: line, 133 | column: column, 134 | }) 135 | if (originPosition.source) { 136 | if (column) { 137 | return originPosition.source + ':' + originPosition.line + ':' + originPosition.column 138 | } else { 139 | return originPosition.source + ':' + originPosition.line 140 | } 141 | } 142 | return _ 143 | }) 144 | consumer.destroy() 145 | return msg 146 | } 147 | -------------------------------------------------------------------------------- /src/lang/javascript.ts: -------------------------------------------------------------------------------- 1 | import { CaseList, getFuncNames, readFileAsync } from '../common/util' 2 | import { config } from '../config' 3 | import * as vscode from 'vscode' 4 | import { BaseLang } from './base' 5 | import { execTestChildProcess } from '../execTestCode' 6 | import { TestOptions, LanguageMetaData, DebugOptions } from '../common/lang' 7 | import { main } from '../debugTask/index' 8 | import path = require('path') 9 | import { outBoundArrayPlugin } from '../babelPlugin' 10 | import babel = require('@babel/core') 11 | import rollup = require('rollup') 12 | import resolve from '@rollup/plugin-node-resolve' 13 | import rollupBabelPlugin from '@rollup/plugin-babel' 14 | import { window } from 'vscode' 15 | import { addComment } from '../common/transformCode' 16 | const virtual = require('@rollup/plugin-virtual') 17 | export class JavascriptParse extends BaseLang { 18 | static getPreImport() { 19 | return '' 20 | } 21 | funcRegExp = /^(?:(\s*function)|(.*=\s*function))/ 22 | testRegExp = /\/\/\s*@test\(((?:"(?:\\.|[^"])*"|[^)])*)\)/ 23 | 24 | async runMultiple(caseList: CaseList, originCode: string, _funcName: string) { 25 | const metaData = (await this.getQuestionMeta()) as LanguageMetaData | undefined 26 | if (!metaData) { 27 | throw new Error('question meta not found') 28 | } 29 | const options: TestOptions = { 30 | caseList, 31 | originCode, 32 | filePath: this.filePath, 33 | metaData: metaData, 34 | } 35 | return await execTestChildProcess(options) 36 | } 37 | 38 | shouldRemoveInBuild(_line: string): boolean { 39 | return false 40 | } 41 | 42 | async runInNewContext(_args: string[], _originCode: string, _funcName: string) { 43 | return '' 44 | } 45 | async handlePreImport() { 46 | return 47 | } 48 | async buildCode() { 49 | try { 50 | const filePath = this.filePath 51 | const text = this.text! 52 | const dir = path.parse(filePath).dir 53 | const { funcNames, questionMeta } = getFuncNames(text, filePath) 54 | const funcRunStr = 'console.log(' + funcNames.map((f) => f + '()').join('+') + ')' 55 | // The rollup will not transform code in virtual entry 56 | const entry: babel.BabelFileResult | null = await babel.transformAsync(text, { 57 | comments: false, 58 | compact: false, 59 | plugins: [outBoundArrayPlugin], 60 | }) 61 | const entryCode = entry?.code + `\n${funcRunStr}` 62 | const bundle = await rollup.rollup({ 63 | input: 'entry', 64 | 65 | treeshake: true, 66 | plugins: [ 67 | // It use virtual entry because treeshake will remove unuse code. 68 | virtual({ 69 | entry: entryCode, 70 | }), 71 | resolve({ rootDir: dir }), 72 | rollupBabelPlugin({ 73 | babelHelpers: 'bundled', 74 | comments: false, 75 | shouldPrintComment: () => false, 76 | }), 77 | ], 78 | }) 79 | const { output } = await bundle.generate({}) 80 | let code = output[0].code 81 | code = code.replace(funcRunStr, '').replace(/;\s*$/, '') 82 | return { 83 | code, 84 | questionMeta, 85 | } 86 | } catch (err) { 87 | console.log('err:', err) 88 | window.showInformationMessage(`parse params err: ${err}`) 89 | return { 90 | code: '', 91 | questionMeta: {}, 92 | } 93 | } 94 | } 95 | // do some thing before debug,eg. get testcase 96 | async beforeDebug(breaks: vscode.SourceBreakpoint[]) { 97 | const args = await this.resolveArgsFromBreaks(breaks) 98 | const metaData = (await this.getQuestionMeta()) as LanguageMetaData | undefined 99 | if (!metaData) { 100 | throw new Error('question meta not found') 101 | } 102 | const originCode = await readFileAsync(this.filePath, { 103 | encoding: 'utf8', 104 | }) 105 | const options: DebugOptions = { 106 | originCode, 107 | filePath: this.filePath, 108 | metaData: metaData, 109 | } 110 | await this.writeTestCase(options, args) 111 | } 112 | async writeTestCase(options: DebugOptions, args: string[]) { 113 | return main(options, args) 114 | } 115 | getDebugConfig() { 116 | const { nodeBinPath } = config 117 | 118 | return { 119 | type: 'node', 120 | request: 'launch', 121 | name: 'debug question', 122 | skipFiles: ['/**'], 123 | program: '${workspaceFolder}/out/code.js', 124 | outFiles: ['${workspaceFolder}/out/*.js'], 125 | runtimeVersion: 'default', 126 | runtimeExecutable: nodeBinPath, 127 | sourceMaps: true, 128 | args: ['${file}'], 129 | // "preLaunchTask": "algorithm: build" 130 | } 131 | } 132 | public addComment(text: string, comment: string, funcName: string) { 133 | return addComment(text, comment, funcName) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/history/index.ts: -------------------------------------------------------------------------------- 1 | import { answerStorage } from './answer' 2 | import { submitStorage } from './storage' 3 | import { config } from '../config' 4 | import { HistoryType, Lang, UpdateCommentOption } from '../model/common' 5 | import { api } from '../api/index' 6 | 7 | export async function getHistory(questionId: string, fn: (code: string, lang: string) => string) { 8 | const originAnswers = await answerStorage.read(questionId) 9 | const formatAnswers = originAnswers 10 | .map((v) => { 11 | return { 12 | code: fn(v.code, v.lang || ''), 13 | obj: { 14 | desc: v.desc, 15 | timestamp: formatTimestamp(v.timestamp), 16 | id: v.id, 17 | lang: v.lang, 18 | }, 19 | } 20 | }) 21 | .reverse() 22 | const answerData = { 23 | header: [ 24 | { 25 | label: 'description', 26 | key: 'desc', 27 | }, 28 | { 29 | label: 'lang', 30 | key: 'lang', 31 | }, 32 | { 33 | label: 'timestamp', 34 | key: 'timestamp', 35 | }, 36 | ], 37 | arr: formatAnswers, 38 | } 39 | const originSubmitStorage = await submitStorage.read(questionId) 40 | const formatSubmits = originSubmitStorage 41 | .map((v) => { 42 | return { 43 | code: fn(v.code, v.submission.lang), 44 | obj: { 45 | ...v.submission, 46 | memory: v.submission.memory, 47 | comment: v.submission.submissionComment, 48 | statusDisplay: v.submission.statusDisplay, 49 | }, 50 | } 51 | }) 52 | .reverse() 53 | 54 | const submitStorageData = { 55 | header: [ 56 | { 57 | label: 'statusDisplay', 58 | key: 'statusDisplay', 59 | }, 60 | { 61 | label: 'lang', 62 | key: 'lang', 63 | }, 64 | { 65 | label: 'memory', 66 | key: 'memory', 67 | }, 68 | { 69 | label: 'runtime', 70 | key: 'runtime', 71 | }, 72 | { 73 | label: 'comment', 74 | key: 'comment', 75 | }, 76 | ], 77 | arr: formatSubmits, 78 | } 79 | 80 | return { 81 | id: questionId, 82 | answerData: answerData, 83 | localSubmit: submitStorageData, 84 | // remoteSubmit: remoteStorageData 85 | } 86 | } 87 | export async function getRemoteSubmits(questionId: string) { 88 | const question = await api.fetchQuestionDetailById(questionId) 89 | const res = await api.fetchSubmissions({ titleSlug: question.titleSlug }) 90 | const submissions = res.submissionList.submissions || [] 91 | const formatRemoteSubmits = submissions.map((v) => { 92 | return { 93 | code: '', 94 | obj: { 95 | ...v, 96 | memory: v.memory, 97 | timestamp: formatTimestamp(v.timestamp), 98 | comment: v?.submissionComment?.comment, 99 | id: v.id, 100 | }, 101 | } 102 | }) 103 | const remoteStorageData = { 104 | header: [ 105 | { 106 | label: 'statusDisplay', 107 | key: 'statusDisplay', 108 | }, 109 | { 110 | label: 'lang', 111 | key: 'lang', 112 | }, 113 | { 114 | label: 'memory', 115 | key: 'memory', 116 | }, 117 | { 118 | label: 'runtime', 119 | key: 'runtime', 120 | }, 121 | { 122 | label: 'comment', 123 | key: 'comment', 124 | }, 125 | ], 126 | arr: formatRemoteSubmits, 127 | } 128 | return remoteStorageData 129 | } 130 | export function formatTimestamp(time: string | number) { 131 | if (typeof time === 'string') { 132 | time = parseInt(time) 133 | } 134 | const num = time * 1000 135 | const date = new Date(num) 136 | const year = date.getFullYear() 137 | const month = (date.getMonth() + 1).toString().padStart(2, '0') 138 | const day = date.getDate().toString().padStart(2, '0') 139 | const hours = date.getHours().toString().padStart(2, '0') 140 | const minutes = date.getMinutes().toString().padStart(2, '0') 141 | if (config.lang === Lang.cn) { 142 | return `${year}/${month}/${day} ${hours}:${minutes}` 143 | } else { 144 | return `${year}/${month}/${day} ${hours}:${minutes}` 145 | } 146 | } 147 | export function formatMemory(memory: number) { 148 | return Math.floor(memory / (1024 * 1024)) + 'M' 149 | } 150 | 151 | // export function updateComment(type: HistoryType.Answer | HistoryType.LocalSubmit, options: UpdateCommentOption): Promise 152 | // export function updateComment(type: HistoryType.RemoteSubmit, options: UpdateRemoteCommentOption): Promise 153 | 154 | export async function updateComment(type: HistoryType, options: UpdateCommentOption): Promise { 155 | switch (type) { 156 | case HistoryType.Answer: { 157 | return answerStorage.updateComment(options) 158 | } 159 | case HistoryType.LocalSubmit: { 160 | return submitStorage.updateComment(options) 161 | } 162 | case HistoryType.RemoteSubmit: { 163 | return submitStorage.updateRemoteComment(options) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/provider/resolver.ts: -------------------------------------------------------------------------------- 1 | import { api, getTags, getQuestionsByTag, getQuestionsByDifficult, getAllQuestions } from '../api/api' 2 | import { DailyWeekMap } from '../model/question' 3 | import { QuestionTree } from './questionsProvider' 4 | import { normalizeQuestions } from '../common/util' 5 | 6 | export interface ResolverParam { 7 | tag: string 8 | titleSlug: string 9 | weekname: string 10 | questionId: number 11 | chapterId: string 12 | itemId: string 13 | } 14 | 15 | interface ResolverReturn extends Pick { 16 | isLast?: boolean 17 | isAC?: boolean 18 | paidOnly?: boolean 19 | } 20 | export type ResolverFn = (param: Partial) => Promise | ResolverReturn[] 21 | 22 | interface RootResolver { 23 | Query: () => Promise | ResolverReturn[] 24 | } 25 | export interface ResolverTypeBase { 26 | [key: string]: ResolverFn | ResolverTypeBase 27 | } 28 | export type ResolverType = ResolverTypeBase & RootResolver 29 | 30 | export const resolverEn: ResolverType = { 31 | Query() { 32 | //'Company', 'Favorite', 33 | return ['All', 'Difficulty', 'Tag', 'Contest', 'DailyChallenge'].map((v) => ({ 34 | type: 'Catalogue', 35 | key: v.toLowerCase(), 36 | label: v, 37 | id: v, 38 | })) 39 | }, 40 | Catalogue: { 41 | async all() { 42 | const questions = await getAllQuestions() 43 | return normalizeQuestions(questions, 'all') 44 | }, 45 | difficulty() { 46 | return ['Easy', 'Medium', 'Hard'].map((v) => ({ 47 | type: 'Difficulty', 48 | key: v.toLowerCase(), 49 | label: v, 50 | id: v, 51 | })) 52 | }, 53 | async tag() { 54 | const tags = await getTags() 55 | return tags.map((tag) => ({ 56 | type: 'Tag', 57 | key: 'tagkey', 58 | label: tag, 59 | id: tag, 60 | param: { 61 | tag, 62 | }, 63 | })) 64 | }, 65 | 66 | // company() { 67 | 68 | // }, 69 | // favorite() { 70 | 71 | // }, 72 | async contest() { 73 | const contests = await api.fetchContests() 74 | return contests.map((contest) => ({ 75 | type: 'Contest', 76 | key: 'contestKey', 77 | label: contest.title, 78 | id: contest.title, 79 | param: { 80 | titleSlug: contest.titleSlug, 81 | }, 82 | })) 83 | }, 84 | async dailychallenge() { 85 | const { chapters } = await api.fetchChapters() 86 | return chapters.map((chapter) => ({ 87 | type: 'Chapter', 88 | label: chapter.title, 89 | id: chapter.title, 90 | param: { 91 | // titleSlug: chapter.slug, 92 | chapterId: chapter.id, 93 | }, 94 | })) 95 | }, 96 | }, 97 | async Chapter({ chapterId }) { 98 | if (!chapterId) { 99 | return [] 100 | } 101 | const chapterDetail = await api.fetchChapter(chapterId) 102 | const chaptersProgressRes = await api.fetchChapterProgress() 103 | const progress = chaptersProgressRes?.getOrCreateExploreSession?.progress 104 | let progressMap: DailyWeekMap | null = null 105 | try { 106 | progressMap = JSON.parse(progress) 107 | } catch (err) { 108 | progressMap = null 109 | } 110 | 111 | const items = chapterDetail.chapter.items 112 | return items.map((item) => { 113 | return { 114 | type: 'DailyQuestion', 115 | label: item.title, 116 | id: 'DailyQuestion' + item.title, 117 | isLast: true, 118 | isAC: progressMap ? progressMap[chapterId][item.id].is_complete : false, 119 | paidOnly: item.paidOnly, 120 | param: { 121 | itemId: item.id, 122 | }, 123 | } 124 | }) 125 | }, 126 | Tag: { 127 | async tagkey({ tag }) { 128 | if (!tag) { 129 | return [] 130 | } 131 | const questions = await getQuestionsByTag(tag) 132 | return normalizeQuestions(questions, 'tag' + tag) 133 | }, 134 | }, 135 | 136 | Difficulty: { 137 | async easy() { 138 | const key = 'easy' 139 | const questions = await getQuestionsByDifficult(key) 140 | return normalizeQuestions(questions, key) 141 | }, 142 | async medium() { 143 | const key = 'medium' 144 | const questions = await getQuestionsByDifficult(key) 145 | return normalizeQuestions(questions, key) 146 | }, 147 | async hard() { 148 | const key = 'hard' 149 | const questions = await getQuestionsByDifficult(key) 150 | return normalizeQuestions(questions, key) 151 | }, 152 | }, 153 | // Company() { 154 | 155 | // }, 156 | // Favorite() { 157 | 158 | // }, 159 | Contest: { 160 | async contestKey({ titleSlug }) { 161 | if (!titleSlug) { 162 | return [] 163 | } 164 | const data = await api.fetchContest(titleSlug) 165 | const questions = data.questions 166 | return questions.map((question) => ({ 167 | type: 'QuestionContest', 168 | label: question.id + ' ' + question.title, 169 | id: 'QuestionContest' + question.id, 170 | isLast: true, 171 | param: { 172 | titleSlug: question.title_slug, 173 | fatherTitleSlug: titleSlug, 174 | }, 175 | })) 176 | }, 177 | }, 178 | } 179 | -------------------------------------------------------------------------------- /src/lang/typescript.ts: -------------------------------------------------------------------------------- 1 | import { CaseList, getFuncNames, readFileAsync } from '../common/util' 2 | import { config } from '../config' 3 | import * as vscode from 'vscode' 4 | import { BaseLang } from './base' 5 | import { execTestChildProcess } from '../execTestCode' 6 | import { TestOptions, LanguageMetaData, DebugOptions } from '../common/lang' 7 | import { main } from '../debugTask/index' 8 | import * as path from 'path' 9 | import babel = require('@babel/core') 10 | import presetTs = require('@babel/preset-typescript') 11 | import { outBoundArrayPlugin } from '../babelPlugin' 12 | import rollup = require('rollup') 13 | import resolve from '@rollup/plugin-node-resolve' 14 | import rollupBabelPlugin from '@rollup/plugin-babel' 15 | import { window } from 'vscode' 16 | import { addComment } from '../common/transformCode' 17 | const virtual = require('@rollup/plugin-virtual') 18 | export class TypescriptParse extends BaseLang { 19 | static getPreImport() { 20 | return '' 21 | } 22 | funcRegExp = /^(?:(\s*function)|(.*=\s*function))/ 23 | testRegExp = /\/\/\s*@test\(((?:"(?:\\.|[^"])*"|[^)])*)\)/ 24 | 25 | async runMultiple(caseList: CaseList, originCode: string, _funcName: string) { 26 | const metaData = (await this.getQuestionMeta()) as LanguageMetaData | undefined 27 | if (!metaData) { 28 | throw new Error('question meta not found') 29 | } 30 | const options: TestOptions = { 31 | caseList, 32 | originCode, 33 | filePath: this.filePath, 34 | metaData: metaData, 35 | } 36 | return await execTestChildProcess(options) 37 | } 38 | async buildCode() { 39 | try { 40 | const filePath = this.filePath 41 | let text = this.text! 42 | text = text 43 | .split('\n') 44 | .filter((line) => !this.shouldRemoveInBuild(line)) 45 | .join('\n') 46 | 47 | const dir = path.parse(filePath).dir 48 | const { funcNames, questionMeta } = getFuncNames(text, filePath) 49 | const funcRunStr = 'console.log(' + funcNames.map((f) => f + '()').join('+') + ')' 50 | // The rollup will not transform code in virtual entry 51 | const entry: babel.BabelFileResult | null = await babel.transformAsync(text, { 52 | filename: filePath, 53 | comments: false, 54 | compact: false, 55 | presets: [presetTs], 56 | plugins: [outBoundArrayPlugin], 57 | }) 58 | const entryCode = entry?.code + `\n${funcRunStr}` 59 | const bundle = await rollup.rollup({ 60 | input: 'entry', 61 | 62 | treeshake: true, 63 | plugins: [ 64 | // It use virtual entry because treeshake will remove unuse code. 65 | virtual({ 66 | entry: entryCode, 67 | }), 68 | 69 | resolve({ rootDir: dir, modulesOnly: true }), 70 | 71 | rollupBabelPlugin({ 72 | babelHelpers: 'bundled', 73 | comments: false, 74 | shouldPrintComment: () => false, 75 | }), 76 | ], 77 | }) 78 | const { output } = await bundle.generate({}) 79 | let code = output[0].code 80 | code = code.replace(funcRunStr, '').replace(/;\s*$/, '') 81 | return { 82 | code, 83 | questionMeta, 84 | } 85 | } catch (err) { 86 | console.log('err:', err) 87 | window.showInformationMessage(`parse params err: ${err}`) 88 | return { 89 | code: '', 90 | questionMeta: {}, 91 | } 92 | } 93 | } 94 | shouldRemoveInBuild(line: string): boolean { 95 | return /import\s*{\s*(ListNode|TreeNode)\s*}\s*from\s*'algm'/.test(line.trimLeft()) 96 | } 97 | 98 | async runInNewContext(_args: string[], _originCode: string, _funcName: string) { 99 | return '' 100 | } 101 | async handlePreImport() { 102 | return 103 | } 104 | 105 | // do some thing before debug,eg. get testcase 106 | async beforeDebug(breaks: vscode.SourceBreakpoint[]) { 107 | const args = await this.resolveArgsFromBreaks(breaks) 108 | const metaData = (await this.getQuestionMeta()) as LanguageMetaData | undefined 109 | if (!metaData) { 110 | throw new Error('question meta not found') 111 | } 112 | const originCode = await readFileAsync(this.filePath, { 113 | encoding: 'utf8', 114 | }) 115 | const options: DebugOptions = { 116 | originCode, 117 | filePath: this.filePath, 118 | metaData: metaData, 119 | } 120 | await this.writeTestCase(options, args) 121 | } 122 | async writeTestCase(options: DebugOptions, args: string[]) { 123 | return main(options, args) 124 | } 125 | 126 | getDebugConfig() { 127 | const { nodeBinPath } = config 128 | 129 | return { 130 | type: 'node', 131 | request: 'launch', 132 | name: 'debug question', 133 | skipFiles: ['/**'], 134 | program: '${workspaceFolder}/out/code.js', 135 | outFiles: ['${workspaceFolder}/out/*.js'], 136 | runtimeVersion: 'default', 137 | runtimeExecutable: nodeBinPath, 138 | sourceMaps: true, 139 | args: ['${file}'], 140 | // "preLaunchTask": "algorithm: build" 141 | } 142 | } 143 | public addComment(text: string, comment: string, funcName: string) { 144 | return addComment(text, comment, funcName) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /docs/README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # algorithm 2 | 3 | An excellent vscode extension for leetcode. 4 | 5 | ## Quick Start 6 | 7 | ![Quick Start](../images/debug.gif) 8 | 9 | ## Support 10 | 11 | - javascript 12 | - typescript 13 | - python3 14 | - golang 15 | - java 16 | - c++ 17 | - shell 18 | - sql 19 | 20 | **将会很快支持其它语言** 21 | 22 | ## Main Features 23 | 24 | - 代码里包含默认测试用例,也可以添加自定义的测试用例,点击测试按钮 自动在本地运行,支持链表和二叉树。 25 | 26 | - debug 指定测试用例 27 | 28 | - 备忘录 29 | 30 | - 一题多解和提交历史 31 | 32 | - 支持 import module, 打包代码, 快速复制打包的代码.(此功能仅支持 js/ts) 33 | 34 | - 包含周赛 (正式比赛时直接使用 submit 是无效的,应该使用复制代码,然后在浏览器提交.) 35 | 36 | ## Other Features 37 | 38 | - 搜索题目 39 | - 每日一题 40 | - 支持同时登录 https://leetcode.com/ 和 https://leetcode.cn/ 41 | 42 | ## 运行条件 43 | 44 | - javascript/typescript 45 | 46 | - Nodejs 12+ 47 | 48 | > 默认会使用 node 执行测试和 debug,如果是使用 nvm 安装的,需要设置选项`algorithm.nodePath` 49 | 50 | - python3 51 | 52 | - python3 53 | - 安装官方 Python 插件(https://code.visualstudio.com/docs/python/python-tutorial) 54 | 55 | - golang 56 | 57 | - 确保 `go` 在环境变量里 58 | - 安装 golang 官方插件 59 | 60 | - java 61 | 62 | - 确保`java`,`javac`在环境变量里,或者[设置 javaPath 和 javacPath](#Setting). 63 | - 安装 java 官方插件(https://code.visualstudio.com/docs/java/java-tutorial) 64 | 65 | - c++ 66 | 67 | - 安装 GCC C++编译器(g++).确保`g++`在环境变量里. 68 | - 安装 c++插件.(https://code.visualstudio.com/docs/cpp/config-linux) 69 | 70 | ## 选择题目 71 | 72 | 在 `algorithm view` 选择题目 73 | 74 | ## 切换语言 75 | 76 | 按 `ctrl+shift+p` 选择 `algorithm:switch default language`,目前支持 javascript,typescript,python3,golang,java,c++ 77 | 78 | ## 切换数据库 79 | 80 | 按 `ctrl+shift+p` 选择 `algorithm:switch default database`,选择 `MySQL`,`MS SQL Server`,`Oracle` 81 | 82 | ## 快捷按钮 83 | 84 | > `view toolbar`有以下几种按钮 85 | 86 | - refresh question 87 | - search question 88 | - login in 89 | - switch endpoint 90 | - collapse all 91 | 92 | ![shortcut buttons](../images/shortcut.png) 93 | 94 | ## 切换 leetcode 版本 95 | 96 | ### 目前支持: 97 | 98 | - leetcode.com 99 | - leetcode.cn 100 | 101 | > Note: 账户不互通. 102 | 103 | ## 登录 104 | 105 | 登录方式参考了官方插件 [vscode-leetcode](https://github.com/LeetCode-OpenSource/vscode-leetcode). 106 | 107 | - leetcode.com 支持 github 和 cookie. 108 | - leetcode.cn 支持 账号密码,github 和 cookie. 109 | 110 | ## 运行测试 111 | 112 | 代码里包含默认测试用例,也可以自定义添加测试用例,点击测试按钮 自动在本地运行。 113 | 114 | ![test](../images/test.png) 115 | 116 | ## Debug 117 | 118 | 只需要在一个测试用例前设置断点,然后给函数添加断点,点击 debug 按钮,便可以运行 debug。 119 | 支持 javascript,typescript,python3,golang,java。 120 | 121 | :tada: 122 | 123 | ![debug](../images/debug.png) 124 | 125 | > Note: 如果你正在调试 java,确保已经打开的问题没有语法错误,否则将会编译失败,因为所有问题在同一个目录下,而 java 相关插件只支持一个目录一个项目。比如你打开了问题 1,但是没有补全代码,然后开始 debug 问题 2,这时候会报语法错误,因为问题 1 应该返回一个值。如果语法正确后编译仍然失败尝试按住 `ctrl+shift+p` ,然后执行命令 `Java:Clean Java Language Server Workspace`. 126 | 127 | > Note:如果你在 mac m1 上调试 c++, 需要安装 vscode 插件 `CodeLLDB` 128 | 129 | ## 提交 130 | 131 | 点击 submit 提交代码. 你可以使用 build 查看最终提交的代码. 132 | 133 | ![build.png](../images/build.png) 134 | 135 | ## 备忘录 136 | 137 | 可以在备忘录里新建不同文件夹,例如 dfs,bfs 等,然后将题目添加进去 138 | ![memo](../images/memo.gif) 139 | 140 | ## 一题多解和提交历史 141 | 142 | ![solution](./images/history.png) 143 | 144 | 1. 点击 history,右边将会出现多种解法列表和提交历史。 145 | 2. 在代码里添加注释 `@desc $(method name)`,在保存多解和提交的时候会自动生成备注。 146 | 3. 右击鼠标可以看到命令`new leetcode answer`。这个命令将会保存解答,同时初始化新的解答。保存的解答可以在 4 中查看。 147 | 4. 保存的多种解答 148 | 5. 保存在本地的提交 149 | 6. 保存在 leetcode 的提交 150 | 151 | ### **注意!** 152 | 153 | 正式比赛时直接使用 submit 是无效的,应该先点击 build,再点击 copy, 最后在浏览器提交. 154 | 155 | ### build code & copy code 156 | 157 | - 点击 build 158 | 159 | ![build.png](../images/build.png) 160 | 161 | - 点击 copy 162 | 163 | ![copy.png](../images/copy.png) 164 | 165 | > 如果使用 js/ts 的话, 会自动导入包 [algm](https://github.com/supperchong/algm) ,包含了许多方便的函数和数据结构, 如 优先队列,线段树, 并查集 ,跳跃表等等. :rocket: 由于 treeshake , 打包后的代码不包含多余的代码. 166 | 167 | ## Setting 168 | 169 | 按 `ctrl+,` 或者打开 `file->Preferences->Settings`。 170 | 选择 `Extensions->algorithm`,将会看到: 171 | 172 | | Setting Name | Description | Default Value | 173 | | --------------- | ------------------------------------------------ | ------------- | 174 | | Auto Import Str | 字符串的值将会插入到代码前面 | "" | 175 | | Base Dir | 题目保存目录 | $HOME/.alg | 176 | | Code Lang | 使用的语言 | JavaScript | 177 | | Lang | 使用指定的终端 | leetcode.com | 178 | | Node Path | nodejs 可执行文件的路径. eg: /usr/local/bin/node | node | 179 | | javaPath | java 可执行文件的路径. | java | 180 | | javacPath | javac 可执行文件的路径. | javac | 181 | | Database | 执行 sql 所用的数据库 | MySQL | 182 | | displayLock | 展示锁住的题目 | false | 183 | 184 | ## 感谢 185 | 186 | 感谢官方插件[vscode-leetcode](https://github.com/LeetCode-OpenSource/vscode-leetcode), 本插件参考了登录方式和一些设计 187 | -------------------------------------------------------------------------------- /src/provider/codelensProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { detectEnableExt, TestCaseParam } from '../common/util' 3 | import { getFileLang, getFileLangSlug, isAlgorithm, isSupportFile } from '../common/langConfig' 4 | import { Service } from '../lang/common' 5 | import { 6 | CodeLensesOptions, 7 | DebugLensesOptions, 8 | GetDescriptionCodeLensesOptions, 9 | SubmitCodeLensesOptions, 10 | TestCodeLensesOptions, 11 | ViewSubmitHistoryCodeLensesOptions, 12 | } from '../model/command' 13 | export class CodelensProvider implements vscode.CodeLensProvider { 14 | private codeLenses: vscode.CodeLens[] = [] 15 | private regex: RegExp 16 | private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter() 17 | public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event 18 | 19 | constructor() { 20 | this.regex = /(@test)/g 21 | 22 | vscode.workspace.onDidChangeConfiguration((_) => { 23 | this._onDidChangeCodeLenses.fire() 24 | }) 25 | } 26 | private getBuildCodeLenses(options: CodeLensesOptions): vscode.CodeLens { 27 | const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7)) 28 | 29 | codeLens.command = { 30 | title: 'build', 31 | tooltip: 'build', 32 | command: 'algorithm.buildCode', 33 | arguments: [options], 34 | } 35 | return codeLens 36 | } 37 | private getSubmitCodeLenses(options: SubmitCodeLensesOptions): vscode.CodeLens { 38 | const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7)) 39 | 40 | codeLens.command = { 41 | title: 'submit', 42 | tooltip: 'submit', 43 | command: 'algorithm.submit', 44 | arguments: [options], 45 | } 46 | return codeLens 47 | } 48 | private getDescriptionCodeLenses(options: GetDescriptionCodeLensesOptions) { 49 | const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7)) 50 | codeLens.command = { 51 | title: 'description', 52 | tooltip: 'description', 53 | command: 'algorithm.getDescription', 54 | arguments: [options], 55 | } 56 | return codeLens 57 | } 58 | private getHistoryCodeLenses(options: ViewSubmitHistoryCodeLensesOptions) { 59 | const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7)) 60 | codeLens.command = { 61 | title: 'history', 62 | tooltip: 'history', 63 | command: 'algorithm.viewSubmitHistory', 64 | arguments: [options], 65 | } 66 | return codeLens 67 | } 68 | private getTestCodeLenses(options: TestCodeLensesOptions) { 69 | const { 70 | testCaseParam: { line }, 71 | } = options 72 | const codeLens = new vscode.CodeLens(new vscode.Range(line, 0, line, 7)) 73 | codeLens.command = { 74 | title: 'test', 75 | tooltip: 'test', 76 | command: 'algorithm.testCode', 77 | arguments: [options], 78 | } 79 | return codeLens 80 | } 81 | private getDebugCodeLenses(testCaseParam: TestCaseParam, filePath: DebugLensesOptions) { 82 | const { line } = testCaseParam 83 | const codeLens = new vscode.CodeLens(new vscode.Range(line, 0, line, 7)) 84 | codeLens.command = { 85 | title: 'debug', 86 | tooltip: 'debug', 87 | command: 'algorithm.debugCode', 88 | arguments: [filePath], 89 | } 90 | return codeLens 91 | } 92 | 93 | public provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] | Thenable { 94 | this.codeLenses = [] 95 | const text = document.getText() 96 | const filePath = document.fileName 97 | const isSupport = isSupportFile(filePath) 98 | if (!isSupport) { 99 | return [] 100 | } 101 | const enableExt = detectEnableExt(text, filePath) 102 | 103 | if (enableExt) { 104 | const codeLang = getFileLang(filePath) 105 | const langSlug = getFileLangSlug(filePath) 106 | const codeLensesOptions: CodeLensesOptions = { 107 | text, 108 | filePath, 109 | langSlug, 110 | } 111 | const submitCodeLenses = this.getSubmitCodeLenses({ 112 | ...codeLensesOptions, 113 | }) 114 | const buildCodeLenses = this.getBuildCodeLenses({ 115 | ...codeLensesOptions, 116 | }) 117 | const desCodeLenses = this.getDescriptionCodeLenses({ 118 | ...codeLensesOptions, 119 | }) 120 | const historyCodeLenses = this.getHistoryCodeLenses({ 121 | ...codeLensesOptions, 122 | }) 123 | this.codeLenses.push(submitCodeLenses) 124 | this.codeLenses.push(buildCodeLenses) 125 | this.codeLenses.push(desCodeLenses) 126 | this.codeLenses.push(historyCodeLenses) 127 | if (isAlgorithm(codeLang)) { 128 | const lang = new Service(filePath, text) 129 | const testCaseList = lang.getTestCaseList(text) 130 | testCaseList.forEach((testCaseParam) => { 131 | const testCodeOptions: TestCodeLensesOptions = { 132 | filePath: document.uri.fsPath, 133 | testCaseParam, 134 | } 135 | const testCodeLenses = this.getTestCodeLenses(testCodeOptions) 136 | const debugCodeLenses = this.getDebugCodeLenses(testCaseParam, document.uri.fsPath) 137 | this.codeLenses.push(testCodeLenses) 138 | this.codeLenses.push(debugCodeLenses) 139 | }) 140 | } 141 | 142 | return this.codeLenses 143 | } 144 | return [] 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/provider/memoProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { config, updateEnv } from '../config' 3 | import { sortFiles } from '../util' 4 | const MemoFilePreviewCommand = 'algorithm.memoFilePreview' 5 | const MemoFileContext = 'memoFile' 6 | enum MemoLevel { 7 | Folder, 8 | File, 9 | Invalid, 10 | } 11 | enum RemoveMemoMsg { 12 | Folder = 'Are you sure you want to delete the folder?', 13 | File = 'Are you sure you want to delete the file?', 14 | } 15 | 16 | export class MemoTree extends vscode.TreeItem { 17 | constructor( 18 | public label: string, 19 | public id: string, 20 | public paths: string[], 21 | public collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.Collapsed, 22 | public command?: vscode.Command 23 | ) { 24 | super(label, vscode.TreeItemCollapsibleState.Collapsed) 25 | } 26 | contextValue = 'memo' 27 | } 28 | 29 | export class MemoProvider implements vscode.TreeDataProvider { 30 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 31 | MemoTree | undefined 32 | >() 33 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event 34 | refresh(): void { 35 | this._onDidChangeTreeData.fire(undefined) 36 | } 37 | getTreeItem(element: MemoTree): vscode.TreeItem { 38 | return element 39 | } 40 | getChildren(element?: MemoTree): Thenable { 41 | if (element) { 42 | const folderName = element.label 43 | const files = this.getFolderFiles(folderName) 44 | return Promise.resolve( 45 | files.map((f) => { 46 | const tree = new MemoTree(f.label, f.id, f.paths, vscode.TreeItemCollapsibleState.None, { 47 | title: 'memoFilePreview', 48 | command: MemoFilePreviewCommand, 49 | arguments: [f.param], 50 | }) 51 | tree.contextValue = MemoFileContext 52 | return tree 53 | }) 54 | ) 55 | } else { 56 | const folders = this.getFolders() 57 | return Promise.resolve( 58 | folders.map((f) => { 59 | const tree = new MemoTree(f.label, f.id, f.paths) 60 | tree.contextValue = MemoFileContext 61 | return tree 62 | }) 63 | ) 64 | } 65 | } 66 | getFolders() { 67 | return config.env.memo.map((v) => { 68 | return { 69 | label: v.name, 70 | id: 'folder' + v.name, 71 | paths: [v.name], 72 | } 73 | }) 74 | } 75 | getFolderFiles(name: string) { 76 | const folder = config.env.memo.find((v) => v.name === name) 77 | if (folder) { 78 | const files = folder.children 79 | sortFiles(files) 80 | return files.map((v) => { 81 | return { 82 | label: v.name, 83 | id: name + v.name, 84 | paths: [name, v.name], 85 | param: v, 86 | } 87 | }) 88 | } else { 89 | return [] 90 | } 91 | } 92 | static getElementLevel(element: MemoTree) { 93 | const paths = element.paths 94 | if (paths.length === 1) { 95 | return MemoLevel.Folder 96 | } else if (paths.length === 2) { 97 | return MemoLevel.File 98 | } 99 | return MemoLevel.Invalid 100 | } 101 | static async remove(element: MemoTree): Promise { 102 | const isRemove = await MemoProvider.checkRemove(element) 103 | if (!isRemove) { 104 | return false 105 | } 106 | const removeConfigs = [ 107 | { 108 | level: MemoLevel.Folder, 109 | fn: MemoProvider.removeFolder, 110 | }, 111 | { 112 | level: MemoLevel.File, 113 | fn: MemoProvider.removeFile, 114 | }, 115 | ] 116 | const level = MemoProvider.getElementLevel(element) 117 | const config = removeConfigs.find((c) => c.level === level) 118 | if (config) { 119 | config.fn(element.paths) 120 | return true 121 | } 122 | return false 123 | } 124 | 125 | static async checkRemove(element: MemoTree): Promise { 126 | const msgConfigs = [ 127 | { 128 | level: MemoLevel.Folder, 129 | msg: RemoveMemoMsg.Folder, 130 | }, 131 | { 132 | level: MemoLevel.File, 133 | msg: RemoveMemoMsg.File, 134 | }, 135 | ] 136 | const Remove = 'remove' 137 | const level = MemoProvider.getElementLevel(element) 138 | const config = msgConfigs.find((c) => c.level === level) 139 | if (config) { 140 | const msg = config.msg 141 | const r = await vscode.window.showWarningMessage(msg, { modal: true }, Remove) 142 | return r === Remove 143 | } 144 | return false 145 | } 146 | static removeFolder(paths: string[]) { 147 | const memo = config.env.memo 148 | const folderName = paths[0] 149 | const index = memo.findIndex((m) => m.name === folderName) 150 | if (index !== -1) { 151 | memo.splice(index, 1) 152 | return updateEnv('memo', memo) 153 | } 154 | 155 | config.log.appendLine('folder not exist') 156 | } 157 | static removeFile(paths: string[]) { 158 | const memo = config.env.memo 159 | const [folderName, fileName] = paths 160 | const folder = memo.find((m) => m.name === folderName) 161 | if (folder) { 162 | const index = folder.children.findIndex((f) => f.name === fileName) 163 | if (index !== -1) { 164 | folder.children.splice(index, 1) 165 | return updateEnv('memo', memo) 166 | } 167 | } 168 | config.log.appendLine('file not exist') 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /template/cpp/parse_test.cpp: -------------------------------------------------------------------------------- 1 | #include "parse.h" 2 | #include 3 | #include "algm.h" 4 | using namespace std; 5 | 6 | string transformBool(bool t) 7 | { 8 | if (t == 1) 9 | { 10 | return "ok"; 11 | } 12 | else 13 | { 14 | return "fail"; 15 | } 16 | } 17 | 18 | bool deepEqual(vector &a, vector &b) 19 | { 20 | if (a.size() != b.size()) 21 | { 22 | return false; 23 | } 24 | for (int i = 0; i < a.size(); i++) 25 | { 26 | if (a[i] != b[i]) 27 | { 28 | return false; 29 | } 30 | } 31 | return true; 32 | } 33 | bool deepEqual(vector &a, vector &b) 34 | { 35 | if (a.size() != b.size()) 36 | { 37 | return false; 38 | } 39 | for (int i = 0; i < a.size(); i++) 40 | { 41 | if (a[i] != b[i]) 42 | { 43 | return false; 44 | } 45 | } 46 | return true; 47 | } 48 | bool deepEqual(vector> &a, vector> &b) 49 | { 50 | if (a.size() != b.size()) 51 | { 52 | return false; 53 | } 54 | for (int i = 0; i < a.size(); i++) 55 | { 56 | if (!deepEqual(a[i], b[i])) 57 | { 58 | return false; 59 | } 60 | } 61 | return true; 62 | } 63 | bool deepEqual(vector> &a, vector> &b) 64 | { 65 | if (a.size() != b.size()) 66 | { 67 | return false; 68 | } 69 | for (int i = 0; i < a.size(); i++) 70 | { 71 | if (!deepEqual(a[i], b[i])) 72 | { 73 | return false; 74 | } 75 | } 76 | return true; 77 | } 78 | int main() 79 | { 80 | int num = parseInteger("1"); 81 | cout << "test parseInteger: " << transformBool(num == 1) << endl; 82 | 83 | vector nums = parseIntegerArr("[1,2,3]"); 84 | vector target{1, 2, 3}; 85 | cout << "test parseIntegerArr: " << transformBool(deepEqual(nums, target)) << endl; 86 | 87 | vector> nums2 = parseIntegerArrArr("[[1,2,3],[4,5,6]]"); 88 | vector> target2{vector{1, 2, 3}, vector{4, 5, 6}}; 89 | cout << "test parseIntegerArrArr: " << transformBool(deepEqual(nums2, target2)) << endl; 90 | 91 | string str3 = parseString("\"abc\\\"\""); 92 | string target3 = "abc\""; 93 | cout << "test parseString: " << transformBool(str3 == target3) << endl; 94 | 95 | vector str4 = parseStringArr("[\"abc\\\"\"]"); 96 | vector target4{"abc\""}; 97 | cout << "test parseStringArr: " << transformBool(deepEqual(str4, target4)) << endl; 98 | 99 | vector> str5 = parseStringArrArr("[[\"1\",\"2\",\"3\"],[\"4\",\"5\",\"6\"]]"); 100 | vector> target5{vector{"1", "2", "3"}, vector{"4", "5", "6"}}; 101 | cout << "test parseStringArrArr: " << transformBool(deepEqual(str5, target5)) << endl; 102 | 103 | string origin6 = "[1,2,null,3]"; 104 | TreeNode *treeNode6 = parseTreeNode("[1,2,null,3]"); 105 | string target6 = serializeTreeNode(treeNode6); 106 | cout << "test parseTreeNode: " << transformBool(origin6 == target6) << endl; 107 | 108 | string origin7 = "[1,2,3]"; 109 | ListNode *listNode7 = parseListNode(origin7); 110 | string target7 = serializeListNode(listNode7); 111 | cout << "test parseListNode: " << transformBool(origin7 == target7) << endl; 112 | 113 | string origin8 = "[[1,2,3]]"; 114 | vector listNodeArr8 = parseListNodeArr(origin8); 115 | string target8 = serializeListNodeArr(listNodeArr8); 116 | cout << "test parseListNodeArr: " << transformBool(origin8 == target8) << endl; 117 | 118 | string origin9 = "[[1,2,null,3],[1,2,3]]"; 119 | vector listNodeArr9 = parseTreeNodeArr(origin9); 120 | string target9 = serializeTreeNodeArr(listNodeArr9); 121 | cout << "test parseTreeNodeArr: " << transformBool(origin9 == target9) << endl; 122 | 123 | string origin10 = "2.00000"; 124 | double num10 = parseFloat(origin10); 125 | string target10 = serializeFloat(num10); 126 | cout << "test serializeFloat: " << transformBool(origin10 == target10) << endl; 127 | 128 | char char11 = parseChar("\"a\""); 129 | char target11 = 'a'; 130 | cout << "test parseString: " << transformBool(char11 == target11) << endl; 131 | 132 | string origin12 = "\"a\""; 133 | char char12 = parseChar(origin12); 134 | string str12 = serializeChar(char12); 135 | cout << "test parseChar: " << transformBool(str12 == origin12) << endl; 136 | 137 | string origin13 = "[\"a\",\"v\"]"; 138 | vector charArr13 = parseCharArr(origin13); 139 | string str13 = serializeCharArr(charArr13); 140 | cout << "test parseCharArr: " << transformBool(str13 == origin13) << endl; 141 | 142 | string origin14 = "[[\"a\",\"v\"]]"; 143 | vector> charArrArr14 = parseCharArrArr(origin14); 144 | string str14 = serializeCharArrArr(charArrArr14); 145 | cout << "test parseCharArrArr: " << transformBool(str14 == origin14) << endl; 146 | } -------------------------------------------------------------------------------- /media/markdown.css: -------------------------------------------------------------------------------- 1 | /** 2 | * copy from https://github.com/microsoft/vscode/blob/master/extensions/markdown-language-features/media/markdown.css 3 | */ 4 | 5 | /*--------------------------------------------------------------------------------------------- 6 | * Copyright (c) Microsoft Corporation. All rights reserved. 7 | * Licensed under the MIT License. See License.txt in the project root for license information. 8 | *--------------------------------------------------------------------------------------------*/ 9 | 10 | html, body { 11 | font-family: var(--markdown-font-family, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", system-ui, "Ubuntu", "Droid Sans", sans-serif); 12 | font-size: var(--markdown-font-size, 14px); 13 | padding: 0 26px; 14 | line-height: var(--markdown-line-height, 22px); 15 | word-wrap: break-word; 16 | } 17 | 18 | #code-csp-warning { 19 | position: fixed; 20 | top: 0; 21 | right: 0; 22 | color: white; 23 | margin: 16px; 24 | text-align: center; 25 | font-size: 12px; 26 | font-family: sans-serif; 27 | background-color:#444444; 28 | cursor: pointer; 29 | padding: 6px; 30 | box-shadow: 1px 1px 1px rgba(0,0,0,.25); 31 | } 32 | 33 | #code-csp-warning:hover { 34 | text-decoration: none; 35 | background-color:#007acc; 36 | box-shadow: 2px 2px 2px rgba(0,0,0,.25); 37 | } 38 | 39 | body.scrollBeyondLastLine { 40 | margin-bottom: calc(100vh - 22px); 41 | } 42 | 43 | body.showEditorSelection .code-line { 44 | position: relative; 45 | } 46 | 47 | body.showEditorSelection .code-active-line:before, 48 | body.showEditorSelection .code-line:hover:before { 49 | content: ""; 50 | display: block; 51 | position: absolute; 52 | top: 0; 53 | left: -12px; 54 | height: 100%; 55 | } 56 | 57 | body.showEditorSelection li.code-active-line:before, 58 | body.showEditorSelection li.code-line:hover:before { 59 | left: -30px; 60 | } 61 | 62 | .vscode-light.showEditorSelection .code-active-line:before { 63 | border-left: 3px solid rgba(0, 0, 0, 0.15); 64 | } 65 | 66 | .vscode-light.showEditorSelection .code-line:hover:before { 67 | border-left: 3px solid rgba(0, 0, 0, 0.40); 68 | } 69 | 70 | .vscode-light.showEditorSelection .code-line .code-line:hover:before { 71 | border-left: none; 72 | } 73 | 74 | .vscode-dark.showEditorSelection .code-active-line:before { 75 | border-left: 3px solid rgba(255, 255, 255, 0.4); 76 | } 77 | 78 | .vscode-dark.showEditorSelection .code-line:hover:before { 79 | border-left: 3px solid rgba(255, 255, 255, 0.60); 80 | } 81 | 82 | .vscode-dark.showEditorSelection .code-line .code-line:hover:before { 83 | border-left: none; 84 | } 85 | 86 | .vscode-high-contrast.showEditorSelection .code-active-line:before { 87 | border-left: 3px solid rgba(255, 160, 0, 0.7); 88 | } 89 | 90 | .vscode-high-contrast.showEditorSelection .code-line:hover:before { 91 | border-left: 3px solid rgba(255, 160, 0, 1); 92 | } 93 | 94 | .vscode-high-contrast.showEditorSelection .code-line .code-line:hover:before { 95 | border-left: none; 96 | } 97 | 98 | img { 99 | max-width: 100%; 100 | max-height: 100%; 101 | } 102 | 103 | a { 104 | text-decoration: none; 105 | } 106 | 107 | a:hover { 108 | text-decoration: underline; 109 | } 110 | 111 | a:focus, 112 | input:focus, 113 | select:focus, 114 | textarea:focus { 115 | outline: 1px solid -webkit-focus-ring-color; 116 | outline-offset: -1px; 117 | } 118 | 119 | hr { 120 | border: 0; 121 | height: 2px; 122 | border-bottom: 2px solid; 123 | } 124 | 125 | h1 { 126 | padding-bottom: 0.3em; 127 | line-height: 1.2; 128 | border-bottom-width: 1px; 129 | border-bottom-style: solid; 130 | } 131 | 132 | h1, h2, h3 { 133 | font-weight: normal; 134 | } 135 | 136 | table { 137 | border-collapse: collapse; 138 | } 139 | 140 | table > thead > tr > th { 141 | text-align: left; 142 | border-bottom: 1px solid; 143 | } 144 | 145 | table > thead > tr > th, 146 | table > thead > tr > td, 147 | table > tbody > tr > th, 148 | table > tbody > tr > td { 149 | padding: 5px 10px; 150 | } 151 | 152 | table > tbody > tr + tr > td { 153 | border-top: 1px solid; 154 | } 155 | 156 | blockquote { 157 | margin: 0 7px 0 5px; 158 | padding: 0 16px 0 10px; 159 | border-left-width: 5px; 160 | border-left-style: solid; 161 | } 162 | 163 | code { 164 | font-family: var(--vscode-editor-font-family, "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace); 165 | font-size: 1em; 166 | line-height: 1.357em; 167 | } 168 | 169 | body.wordWrap pre { 170 | white-space: pre-wrap; 171 | } 172 | 173 | pre:not(.hljs), 174 | pre.hljs code > div { 175 | padding: 16px; 176 | border-radius: 3px; 177 | overflow: auto; 178 | } 179 | 180 | pre code { 181 | color: var(--vscode-editor-foreground); 182 | tab-size: 4; 183 | } 184 | 185 | /** Theming */ 186 | 187 | .vscode-light pre { 188 | background-color: rgba(220, 220, 220, 0.4); 189 | } 190 | 191 | .vscode-dark pre { 192 | background-color: rgba(10, 10, 10, 0.4); 193 | } 194 | 195 | .vscode-high-contrast pre { 196 | background-color: rgb(0, 0, 0); 197 | } 198 | 199 | .vscode-high-contrast h1 { 200 | border-color: rgb(0, 0, 0); 201 | } 202 | 203 | .vscode-light table > thead > tr > th { 204 | border-color: rgba(0, 0, 0, 0.69); 205 | } 206 | 207 | .vscode-dark table > thead > tr > th { 208 | border-color: rgba(255, 255, 255, 0.69); 209 | } 210 | 211 | .vscode-light h1, 212 | .vscode-light hr, 213 | .vscode-light table > tbody > tr + tr > td { 214 | border-color: rgba(0, 0, 0, 0.18); 215 | } 216 | 217 | .vscode-dark h1, 218 | .vscode-dark hr, 219 | .vscode-dark table > tbody > tr + tr > td { 220 | border-color: rgba(255, 255, 255, 0.18); 221 | } 222 | -------------------------------------------------------------------------------- /src/webview/buildCodeWebview.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as vscode from 'vscode' 3 | import { Clipboard, env } from 'vscode' 4 | import { highlightCode } from '../util' 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | 7 | class BuildCodePanel { 8 | /** 9 | * Track the currently panel. Only allow a single panel to exist at a time. 10 | */ 11 | public static currentPanel: BuildCodePanel | undefined 12 | private static readonly buildCodeActiveContextKey = 'buildCodeFocus' 13 | public static readonly viewType = 'buildCodeView' 14 | 15 | private readonly _panel: vscode.WebviewPanel 16 | private readonly _extensionPath: string 17 | private context: vscode.ExtensionContext 18 | public text: string 19 | public langSlug: string 20 | private clipboard: Clipboard = env.clipboard 21 | private _disposables: vscode.Disposable[] = [] 22 | public static commands = new Map() 23 | private setbuildCodeActiveContext(value: boolean) { 24 | vscode.commands.executeCommand('setContext', BuildCodePanel.buildCodeActiveContextKey, value) 25 | } 26 | private registerCommand(id: string, impl: (...args: unknown[]) => void, thisArg?: unknown) { 27 | if (!BuildCodePanel.commands.get(id)) { 28 | const dispose = vscode.commands.registerCommand(id, impl, thisArg) 29 | this.context.subscriptions.push(dispose) 30 | BuildCodePanel.commands.set(id, dispose) 31 | } 32 | } 33 | public static createOrShow(context: vscode.ExtensionContext, text: string, langSlug: string) { 34 | const column = vscode.ViewColumn.Two 35 | const extensionPath = context.extensionPath 36 | // If we already have a panel, show it. 37 | 38 | if (BuildCodePanel.currentPanel) { 39 | BuildCodePanel.currentPanel._panel.reveal(column) 40 | BuildCodePanel.currentPanel.update(text, langSlug) 41 | return 42 | } 43 | 44 | // Otherwise, create a new panel. 45 | const panel = vscode.window.createWebviewPanel(BuildCodePanel.viewType, 'code', vscode.ViewColumn.Two, { 46 | // Enable javascript in the webview 47 | enableScripts: true, 48 | localResourceRoots: [vscode.Uri.file(path.join(extensionPath, 'media'))], 49 | }) 50 | 51 | BuildCodePanel.currentPanel = new BuildCodePanel(panel, context, text, langSlug) 52 | } 53 | 54 | private constructor(panel: vscode.WebviewPanel, context: vscode.ExtensionContext, text: string, langSlug: string) { 55 | this._panel = panel 56 | this._extensionPath = context.extensionPath 57 | this.context = context 58 | this.text = text 59 | this.langSlug = langSlug 60 | this.registerCommand('algorithm.copyCode', () => { 61 | this.clipboard.writeText(this.text) 62 | }) 63 | 64 | this.initView() 65 | // Set the webview's initial html content 66 | this._update() 67 | 68 | // Listen for when the panel is disposed 69 | // This happens when the user closes the panel or when the panel is closed programatically 70 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables) 71 | 72 | // Update the content based on view changes 73 | this._panel.onDidChangeViewState( 74 | (e) => { 75 | this.setbuildCodeActiveContext(e.webviewPanel.active) 76 | if (e.webviewPanel.active) { 77 | this._update() 78 | } 79 | }, 80 | null, 81 | this._disposables 82 | ) 83 | } 84 | 85 | public dispose() { 86 | BuildCodePanel.currentPanel = undefined 87 | 88 | // Clean up our resources 89 | this._panel.dispose() 90 | this.setbuildCodeActiveContext(false) 91 | while (this._disposables.length) { 92 | const x = this._disposables.pop() 93 | if (x) { 94 | x.dispose() 95 | } 96 | } 97 | } 98 | public update(text: string, langSlug: string) { 99 | this.text = text 100 | this.langSlug = langSlug 101 | this._update() 102 | } 103 | private getWebviewUri(name: string) { 104 | const webview = this._panel.webview 105 | const scriptPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'media', name)) 106 | return webview.asWebviewUri(scriptPathOnDisk) 107 | } 108 | private initView() { 109 | const nonce = getNonce() 110 | 111 | // And the uri we use to load this script in the webview 112 | const buildCodeUri = this.getWebviewUri('buildcode.js') 113 | const scriptUri = this.getWebviewUri('highlight.min.js') 114 | const cssUri = this.getWebviewUri('highlight.css') 115 | this._panel.webview.html = ` 116 | 117 | 118 | 119 | 123 | 124 | 125 | Cat Coding 126 | 127 | 128 | 129 |
130 |
131 | 132 | 133 | 134 | 135 | ` 136 | } 137 | private _update() { 138 | this.setbuildCodeActiveContext(true) 139 | const code = highlightCode(this.text, this.langSlug) 140 | this._panel.webview.postMessage({ command: 'newCode', data: code }) 141 | } 142 | } 143 | 144 | function getNonce() { 145 | let text = '' 146 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 147 | for (let i = 0; i < 32; i++) { 148 | text += possible.charAt(Math.floor(Math.random() * possible.length)) 149 | } 150 | return text 151 | } 152 | 153 | export function createPanel(context: vscode.ExtensionContext, text: string, langSlug: string) { 154 | BuildCodePanel.createOrShow(context, text, langSlug) 155 | } 156 | -------------------------------------------------------------------------------- /src/common/langConfig.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | const langsConfig = { 4 | algorithms: [ 5 | { 6 | lang: 'C++', 7 | langSlug: 'cpp', 8 | ext: '.cpp', 9 | comment: '//', 10 | fileNameSep: '.', 11 | }, 12 | { 13 | lang: 'Java', 14 | langSlug: 'java', 15 | ext: '.java', 16 | comment: '//', 17 | fileNameSep: '.', 18 | }, 19 | { 20 | lang: 'Python', 21 | langSlug: 'python', 22 | ext: '.py', 23 | comment: '#', 24 | fileNameSep: '_', 25 | }, 26 | { 27 | lang: 'Python3', 28 | langSlug: 'python3', 29 | ext: '.py', 30 | comment: '#', 31 | fileNameSep: '_', 32 | }, 33 | { 34 | lang: 'C', 35 | langSlug: 'c', 36 | ext: '.c', 37 | comment: '//', 38 | fileNameSep: '.', 39 | }, 40 | { 41 | lang: 'C#', 42 | langSlug: 'csharp', 43 | ext: '.cs', 44 | comment: '//', 45 | fileNameSep: '.', 46 | }, 47 | { 48 | lang: 'JavaScript', 49 | langSlug: 'javascript', 50 | ext: '.js', 51 | comment: '//', 52 | fileNameSep: '.', 53 | }, 54 | { 55 | lang: 'Ruby', 56 | langSlug: 'ruby', 57 | ext: '.rb', 58 | comment: '#', 59 | fileNameSep: '.', 60 | }, 61 | { 62 | lang: 'Swift', 63 | langSlug: 'swift', 64 | ext: '.swift', 65 | comment: '//', 66 | fileNameSep: '.', 67 | }, 68 | { 69 | lang: 'Go', 70 | langSlug: 'golang', 71 | ext: '.go', 72 | comment: '//', 73 | fileNameSep: '.', 74 | }, 75 | { 76 | lang: 'Scala', 77 | langSlug: 'scala', 78 | ext: '.scala', 79 | comment: '//', 80 | fileNameSep: '.', 81 | }, 82 | { 83 | lang: 'Kotlin', 84 | langSlug: 'kotlin', 85 | ext: '.kt', 86 | comment: '//', 87 | fileNameSep: '.', 88 | }, 89 | { 90 | lang: 'Rust', 91 | langSlug: 'rust', 92 | ext: '.rs', 93 | comment: '//', 94 | fileNameSep: '.', 95 | }, 96 | { 97 | lang: 'PHP', 98 | langSlug: 'php', 99 | ext: '.php', 100 | comment: '//', 101 | fileNameSep: '.', 102 | }, 103 | { 104 | lang: 'TypeScript', 105 | langSlug: 'typescript', 106 | ext: '.ts', 107 | comment: '//', 108 | fileNameSep: '.', 109 | }, 110 | ], 111 | database: [ 112 | { 113 | lang: 'MySQL', 114 | langSlug: 'mysql', 115 | ext: '.sql', 116 | comment: '#', 117 | fileNameSep: '.', 118 | }, 119 | { 120 | lang: 'MS SQL Server', 121 | langSlug: 'mssql', 122 | ext: '.sql', 123 | comment: '#', 124 | fileNameSep: '.', 125 | }, 126 | { 127 | lang: 'Oracle', 128 | langSlug: 'oraclesql', 129 | ext: '.sql', 130 | comment: '#', 131 | fileNameSep: '.', 132 | }, 133 | ], 134 | shell: [ 135 | { 136 | lang: 'Bash', 137 | langSlug: 'bash', 138 | ext: '.sh', 139 | comment: '#', 140 | fileNameSep: '.', 141 | }, 142 | ], 143 | } 144 | enum LangBaseComment { 145 | Slash = '//', 146 | Pound = '#', 147 | } 148 | export interface LangBase { 149 | lang: CodeLang 150 | langSlug: string 151 | ext: string 152 | comment: LangBaseComment 153 | fileNameSep: '.' | '_' 154 | } 155 | export interface LangMap { 156 | [langSlug: string]: LangBase 157 | } 158 | interface LangExtMap { 159 | [ext: string]: LangBase 160 | } 161 | const allLangs = Object.values(langsConfig).reduce((prev, cur) => prev.concat(cur)) 162 | export const langMap: LangMap = allLangs.reduce((prev, cur) => (prev[cur.langSlug] = cur) && prev, {}) 163 | export const langExtMap: LangExtMap = allLangs.reduce((prev, cur) => (prev[cur.ext] = cur) && prev, {}) 164 | export enum CodeLang { 165 | 'C++' = 'C++', 166 | 'C#' = 'C#', 167 | Java = 'Java', 168 | Python = 'Python', 169 | Python3 = 'Python3', 170 | C = 'C', 171 | JavaScript = 'JavaScript', 172 | Ruby = 'Ruby', 173 | Swift = 'Swift', 174 | Go = 'Go', 175 | Scala = 'Scala', 176 | Kotlin = 'Kotlin', 177 | Rust = 'Rust', 178 | PHP = 'PHP', 179 | TypeScript = 'TypeScript', 180 | } 181 | export enum DataBase { 182 | MySQL = 'MySQL', 183 | 'MS SQL Server' = 'MS SQL Server', 184 | 'Oracle' = 'Oracle', 185 | } 186 | 187 | export function getFileLang(filePath: string): CodeLang { 188 | const ext = path.extname(filePath) 189 | const langItem = langExtMap[ext] 190 | if (!langItem) { 191 | throw new Error('file extname invalid') 192 | } 193 | return langItem.lang 194 | } 195 | 196 | export function getFileLangSlug(filePath: string): string { 197 | const ext = path.extname(filePath) 198 | const langItem = langExtMap[ext] 199 | if (!langItem) { 200 | throw new Error('file extname invalid') 201 | } 202 | return langItem.langSlug 203 | } 204 | export function isDataBase(filePath: string): boolean { 205 | const ext = path.extname(filePath) 206 | return ext === '.sql' 207 | } 208 | export function isShell(filePath: string): boolean { 209 | const ext = path.extname(filePath) 210 | return ext === '.sh' 211 | } 212 | export function isAlgorithm(lang: CodeLang): boolean { 213 | return !!langsConfig.algorithms.find((alg) => alg.lang === lang) 214 | } 215 | export function getFileComment(filePath: string): LangBaseComment { 216 | const ext = path.extname(filePath) 217 | const langItem = langExtMap[ext] 218 | if (!langItem) { 219 | throw new Error('file extname invalid') 220 | } 221 | return langItem.comment 222 | } 223 | 224 | export const builtInLang = [CodeLang.JavaScript, CodeLang.TypeScript] 225 | export const otherLang = [CodeLang.Python3, CodeLang.Go, CodeLang.Java, CodeLang['C++']] 226 | export const enableLang = [...builtInLang, ...otherLang] 227 | export const databases = [DataBase['MS SQL Server'], DataBase.MySQL, DataBase.Oracle] 228 | export const enNameLangs = [CodeLang.Java, CodeLang['C++']] 229 | export enum ExtraType { 230 | ListNode = 'ListNode', 231 | TreeNode = 'TreeNode', 232 | } 233 | export const highlightLangMap = { 234 | cpp: 'cpp', 235 | java: 'java', 236 | python: 'python', 237 | python3: 'python', 238 | c: 'c', 239 | csharp: 'csharp', 240 | javascript: 'javascript', 241 | ruby: 'ruby', 242 | swift: 'swift', 243 | golang: 'go', 244 | scala: 'scala', 245 | kotlin: 'kotlin', 246 | rust: 'rust', 247 | php: 'php', 248 | typescript: 'typescript', 249 | } 250 | export function transformToHightlightLang(lang: string): string { 251 | return highlightLangMap[lang] || lang 252 | } 253 | export function isSupportFile(filePath: string) { 254 | if (isShell(filePath) || isDataBase(filePath)) { 255 | return true 256 | } 257 | const ext = path.extname(filePath) 258 | 259 | const langItem = langExtMap[ext] 260 | if (!langItem) { 261 | return false 262 | } 263 | const lang = langItem.lang 264 | return enableLang.includes(lang) 265 | } 266 | -------------------------------------------------------------------------------- /src/model/question.cn.ts: -------------------------------------------------------------------------------- 1 | import { CodeSnippet, FlagType, GraphqlVariables, SubmissionComment } from './common' 2 | 3 | export interface ConciseQuestion { 4 | fid: string 5 | level: number 6 | id: number 7 | title: string 8 | slug: string 9 | acs: number 10 | submitted: number 11 | paid_only: boolean 12 | status: string 13 | name: string 14 | } 15 | export interface MapIdConciseQuestion { 16 | [id: number]: ConciseQuestion 17 | } 18 | 19 | export interface TodayRecordData { 20 | todayRecord: TodayRecord[] 21 | } 22 | export interface TodayRecord { 23 | question: Pick 24 | lastSubmission: LastSubmission 25 | date: string 26 | userStatus: UserStatus 27 | __typename: string 28 | } 29 | interface LastSubmission { 30 | id: string 31 | __typename: string 32 | } 33 | type UserStatus = 'FINISH' | 'NOT_START' 34 | 35 | export interface QuestionTranslationData { 36 | translations: Translation[] 37 | } 38 | export interface Translation { 39 | questionId: string 40 | title: string 41 | } 42 | 43 | export interface GraphqlResponse { 44 | data: T 45 | } 46 | export interface DailyQuestionRecordData { 47 | dailyQuestionRecords: [DailyQuestionRecord] 48 | } 49 | export interface DailyQuestionRecord { 50 | date: string 51 | 52 | question: Pick 53 | userStatus: string 54 | __typename: string 55 | } 56 | 57 | export interface GraphqlRequestData { 58 | operationName: string | null 59 | query: string 60 | variables: GraphqlVariables 61 | } 62 | 63 | export interface QuestionData { 64 | question: Question 65 | } 66 | export interface Question { 67 | questionSourceContent?: string 68 | questionId: string 69 | questionFrontendId: string 70 | boundTopicId: number 71 | title: string 72 | titleSlug: string 73 | content: string 74 | translatedTitle: string 75 | translatedContent: string 76 | isPaidOnly: boolean 77 | difficulty: string 78 | likes: number 79 | dislikes: number 80 | isLiked: unknown 81 | similarQuestions: string 82 | contributors: [] 83 | langToValidPlayground: string 84 | topicTags: TopicTags[] 85 | companyTagStats: unknown 86 | codeSnippets: CodeSnippet[] 87 | stats: string 88 | hints: string[] 89 | solution: Solution 90 | status: Status 91 | sampleTestCase: string 92 | metaData: string 93 | judgerAvailable: boolean 94 | judgeType: string 95 | mysqlSchemas: string[] 96 | enableRunCode: boolean 97 | envInfo: string 98 | book: unknown 99 | isSubscribed: boolean 100 | isDailyQuestion: boolean 101 | dailyRecordStatus: string 102 | editorType: string 103 | ugcQuestionId: unknown 104 | style: string 105 | 106 | // questionTitleSlug: string 107 | } 108 | interface Solution { 109 | canSeeDetail: boolean 110 | id: string 111 | } 112 | type Status = 'ac' | 'notac' | null 113 | 114 | interface TopicTags { 115 | name: string 116 | slug: string 117 | translatedName: string 118 | questions: number[] //question id 119 | __typename: string 120 | } 121 | 122 | export interface ContestData { 123 | allContests: Pick[] 124 | } 125 | interface Contest { 126 | containsPremium: boolean 127 | title: string 128 | cardImg: string 129 | titleSlug: string 130 | description: string 131 | startTime: number 132 | duration: number 133 | originStartTime: number 134 | isVirtual: boolean 135 | company: Company 136 | __typename: string 137 | } 138 | interface Company { 139 | watermark: string 140 | __typename: string 141 | } 142 | 143 | export interface TagData { 144 | topics: TopicTags[] 145 | } 146 | // interface Topics { 147 | // slug: string 148 | // name: string 149 | // questions: number[] 150 | // translatedName: string 151 | // } 152 | 153 | export interface SubmitOptions { 154 | titleSlug: string 155 | typed_code: string 156 | question_id: string 157 | lang?: string 158 | } 159 | export interface SubmitContestOptions extends SubmitOptions { 160 | weekname: string 161 | } 162 | export interface SubmitResponse { 163 | submission_id: number 164 | } 165 | export interface CheckResponse { 166 | status_code: number 167 | lang: string 168 | run_success: boolean 169 | status_runtime: string 170 | memory: number 171 | question_id: string 172 | elapsed_time: number 173 | compare_result: string 174 | code_output: string 175 | std_output: string 176 | last_testcase: string 177 | task_finish_time: number 178 | task_name: string 179 | finished: boolean 180 | status_msg: string 181 | state: State 182 | fast_submit: boolean 183 | total_correct: number 184 | total_testcases: number 185 | submission_id: string 186 | runtime_percentile: number 187 | status_memory: string 188 | memory_percentile: number 189 | pretty_lang: string 190 | } 191 | 192 | export interface CheckContestResponse { 193 | state: State 194 | } 195 | type State = 'PENDING' | 'STARTED' | 'SUCCESS' 196 | 197 | export interface CheckOptions { 198 | submission_id: number 199 | titleSlug: string 200 | } 201 | export interface CheckContestOptions extends CheckOptions { 202 | weekname: string 203 | } 204 | 205 | export interface SubmissionsResponse { 206 | submissionList: SubmissionList 207 | } 208 | export interface SubmissionsOptions { 209 | lastKey?: string 210 | limit?: number 211 | offset?: number 212 | titleSlug: string 213 | } 214 | interface SubmissionList { 215 | lastKey: string 216 | hasNext: boolean 217 | submissions: Submissions[] 218 | } 219 | interface Submissions { 220 | id: string 221 | statusDisplay: string 222 | lang: string 223 | runtime: string 224 | timestamp: string 225 | url: string 226 | isPending: string 227 | memory: string 228 | submissionComment?: SubmissionComment 229 | } 230 | export interface UpdateCommentOptions { 231 | comment: string 232 | flagType?: FlagType 233 | submissionId: string 234 | } 235 | export interface UpdateCommentResponse { 236 | submissionCreateOrUpdateSubmissionComment: SubmissionCreateOrUpdateSubmissionComment 237 | } 238 | 239 | interface SubmissionCreateOrUpdateSubmissionComment { 240 | ok: boolean 241 | } 242 | 243 | export interface SubmissionDetailOptions { 244 | id: string 245 | } 246 | export interface SubmissionDetailResponse { 247 | submissionDetail?: SubmissionDetail 248 | } 249 | interface SubmissionDetail { 250 | id: string 251 | code: string 252 | runtime: string 253 | memory: string 254 | rawMemory: string 255 | statusDisplay: string 256 | timestamp: number 257 | lang: string 258 | passedTestCaseCnt: number 259 | totalTestCaseCnt: number 260 | sourceUrl: string 261 | question: Question 262 | outputDetail: OutputDetail 263 | __typename: string 264 | submissionComment: SubmissionComment 265 | } 266 | 267 | interface OutputDetail { 268 | codeOutput: string 269 | expectedOutput: string 270 | input: string 271 | compileError: string 272 | runtimeError: string 273 | lastTestcase: string 274 | __typename: string 275 | } 276 | -------------------------------------------------------------------------------- /src/login/index.ts: -------------------------------------------------------------------------------- 1 | import _request = require('request') 2 | import { promisify } from 'util' 3 | import { window } from 'vscode' 4 | import { config, DomainCN, DomainEN } from '../config' 5 | import { User } from './input' 6 | import { writeFileAsync } from '../common/util' 7 | import { signInCommand } from '../commands' 8 | 9 | function parseCookie(cookie: string) { 10 | const keyValueStrArr = cookie.split(';') 11 | const map = {} 12 | keyValueStrArr.forEach((kvStr) => { 13 | let [key, value] = kvStr.split('=') 14 | key = key.trim() 15 | value = value.trim() 16 | map[key] = value 17 | }) 18 | return map 19 | } 20 | 21 | async function saveCookie(cookie: string) { 22 | const cookiePath = config.cookiePath 23 | const map = parseCookie(cookie) 24 | const headers = { cookie } 25 | if (map['csrftoken']) { 26 | headers['x-csrftoken'] = map['csrftoken'] 27 | } 28 | await writeFileAsync(cookiePath, JSON.stringify(headers)) 29 | } 30 | // set-cookie 31 | async function saveResCookie(cookies: string[] = []) { 32 | const map = {} 33 | for (let i = 0; i < cookies.length; i++) { 34 | const cookie = cookies[i] 35 | const arr = cookie.split(';') 36 | const kv = arr[0] 37 | let [key, value] = kv.split('=') 38 | key = key.trim() 39 | if (key) { 40 | map[key] = value 41 | } 42 | } 43 | const cookieStr = Object.keys(map) 44 | .map((key) => `${key}=${map[key]}`) 45 | .join('; ') 46 | const headers = { cookie: cookieStr } 47 | if (map['csrftoken']) { 48 | headers['x-csrftoken'] = map['csrftoken'] 49 | } 50 | const cookiePath = config.cookiePath 51 | await writeFileAsync(cookiePath, JSON.stringify(headers)) 52 | } 53 | function parseFormData(html: string) { 54 | const formRegExp = // 55 | const r = formRegExp.exec(html.replace(/\n/g, '')) 56 | if (!r) { 57 | return Promise.reject({ 58 | message: 'the page of login github not found', 59 | code: 3, 60 | }) 61 | } 62 | const formStr = r[0] 63 | const inputRegExp = /]*name="(.*?)"(?:[^>](?!alue))*(?:value="(.*?)")?[^>]*>/g 64 | let inputRegExpRes: RegExpExecArray | null = null 65 | let d = {} 66 | const defaultObj = { 67 | 'webauthn-support': 'supported', 68 | 'webauthn-iuvpaa-support': 'unsupported', 69 | } 70 | while ((inputRegExpRes = inputRegExp.exec(formStr))) { 71 | const name = inputRegExpRes[1] 72 | const value = inputRegExpRes[2] 73 | if (name) { 74 | d[name] = value || '' 75 | } 76 | } 77 | d = Object.assign(d, defaultObj) 78 | return d 79 | } 80 | type RequestAsync = ( 81 | arg1: (_request.UriOptions & _request.CoreOptions) | (_request.UrlOptions & _request.CoreOptions) 82 | ) => Promise<_request.Response> 83 | async function githubAuth(html: string, request: RequestAsync, user) { 84 | let data = parseFormData(html) 85 | data = Object.assign(data, { 86 | login: user.name, 87 | password: user.password, 88 | }) 89 | // github login and then redirect back 90 | const res = await request({ 91 | url: 'https://github.com/session', 92 | form: data, 93 | method: 'POST', 94 | followAllRedirects: true, 95 | headers: { 96 | 'content-type': 'application/x-www-form-urlencoded', 97 | }, 98 | }) 99 | let body = res.body 100 | if (/Incorrect username or password/.test(body)) { 101 | return Promise.reject({ 102 | message: 'Incorrect username or password', 103 | code: 3, 104 | }) 105 | } 106 | // enable two-factor 107 | const twoFactorRegExp = /Two-factor authentication/ 108 | if (twoFactorRegExp.test(body)) { 109 | body = await githubTwoFactorAuth(body, request) 110 | } 111 | return body 112 | } 113 | async function githubTwoFactorAuth(html: string, request: RequestAsync) { 114 | const authenticityTokenTwoFactor = html.match(/name="authenticity_token" value="(.*?)"/) 115 | if (authenticityTokenTwoFactor === null) { 116 | return Promise.reject({ 117 | message: 'get GitHub two-factor page fail', 118 | code: 3, 119 | }) 120 | } 121 | const code = await window.showInputBox({ 122 | prompt: 'input the Authentication code', 123 | }) 124 | if (!code) { 125 | return Promise.reject({ 126 | message: 'cancel login', 127 | code: 2, 128 | }) 129 | } 130 | const data = { 131 | otp: code, 132 | authenticity_token: authenticityTokenTwoFactor[1], 133 | } 134 | const res = await request({ 135 | url: 'https://github.com/sessions/two-factor', 136 | form: data, 137 | method: 'POST', 138 | followAllRedirects: true, 139 | headers: { 140 | 'content-type': 'application/x-www-form-urlencoded', 141 | }, 142 | }) 143 | return res.body 144 | } 145 | async function githubRedirectBack(body: string, request: RequestAsync) { 146 | const redirectRegExp = / 0 { 40 | node := queue[0] 41 | queue = queue[1:] 42 | if len(arr) == 0 { 43 | return root 44 | } 45 | 46 | leftVal := arr[0] 47 | arr = arr[1:] 48 | if leftVal != "null" { 49 | val, _ := strconv.Atoi(leftVal) 50 | left := &TreeNode{Val: val} 51 | node.Left = left 52 | queue = append(queue, left) 53 | } 54 | if len(arr) == 0 { 55 | return root 56 | } 57 | rightVal := arr[0] 58 | arr = arr[1:] 59 | if rightVal != "null" { 60 | val, _ := strconv.Atoi(rightVal) 61 | right := &TreeNode{Val: val} 62 | node.Right = right 63 | queue = append(queue, right) 64 | } 65 | 66 | } 67 | return root 68 | 69 | } 70 | func deserializeTreeNodeArr(data string) []*TreeNode { 71 | length := len(data) 72 | if length <= 4 { 73 | return []*TreeNode{} 74 | } 75 | str := data[1 : length-1] 76 | r := []*TreeNode{} 77 | for i := 0; i < len(str); i++ { 78 | if str[i:i+1] == "[" { 79 | flag := false 80 | j := i + 1 81 | for ; j < len(str); j++ { 82 | if str[j:j+1] == "]" { 83 | r = append(r, deserializeTreeNode(str[i:j+1])) 84 | flag = true 85 | break 86 | } 87 | } 88 | if !flag { 89 | fmt.Print("parse error") 90 | return []*TreeNode{} 91 | } 92 | i = j 93 | } 94 | } 95 | return r 96 | 97 | } 98 | func serializeTreeNode(root *TreeNode) string { 99 | if root == nil { 100 | return "[]" 101 | } 102 | var arr []string 103 | queue := []*TreeNode{root} 104 | for len(queue) > 0 { 105 | node := queue[0] 106 | queue = queue[1:] 107 | if node != nil { 108 | arr = append(arr, strconv.Itoa(node.Val)) 109 | queue = append(queue, node.Left) 110 | queue = append(queue, node.Right) 111 | } else { 112 | arr = append(arr, "null") 113 | } 114 | 115 | } 116 | var i = len(arr) - 1 117 | for arr[i] == "null" { 118 | i-- 119 | } 120 | arr = arr[0 : i+1] 121 | return "[" + strings.Join(arr, ",") + "]" 122 | } 123 | 124 | func serializeTreeNodeArr(arr []*TreeNode) string { 125 | strArr := "[" 126 | for i := 0; i < len(arr); i++ { 127 | strArr += serializeTreeNode(arr[i]) 128 | if i != len(arr)-1 { 129 | strArr += "," 130 | } 131 | } 132 | strArr += "]" 133 | return strArr 134 | } 135 | 136 | func deserializeListNode(data string) *ListNode { 137 | length := len(data) 138 | if length <= 2 { 139 | return nil 140 | } 141 | arr := strings.Split(data[1:length-1], ",") 142 | 143 | c := arr[0] 144 | arr = arr[1:] 145 | val, _ := strconv.Atoi(c) 146 | root := &ListNode{Val: val} 147 | p := root 148 | for len(arr) > 0 { 149 | 150 | c := arr[0] 151 | arr = arr[1:] 152 | val, _ := strconv.Atoi(c) 153 | node := &ListNode{Val: val} 154 | p.Next = node 155 | p = node 156 | } 157 | return root 158 | } 159 | 160 | func deserializeListNodeArr(data string) []*ListNode { 161 | length := len(data) 162 | if length <= 4 { 163 | return []*ListNode{} 164 | } 165 | str := data[1 : length-1] 166 | r := []*ListNode{} 167 | for i := 0; i < len(str); i++ { 168 | if str[i:i+1] == "[" { 169 | flag := false 170 | j := i + 1 171 | for ; j < len(str); j++ { 172 | if str[j:j+1] == "]" { 173 | r = append(r, deserializeListNode(str[i:j+1])) 174 | flag = true 175 | break 176 | } 177 | } 178 | if !flag { 179 | //解析错误 180 | fmt.Print("解析错误") 181 | return []*ListNode{} 182 | } 183 | i = j 184 | } 185 | } 186 | return r 187 | } 188 | func serializeListNode(root *ListNode) string { 189 | var arr []string 190 | p := root 191 | for p != nil { 192 | arr = append(arr, strconv.Itoa(p.Val)) 193 | p = p.Next 194 | } 195 | return "[" + strings.Join(arr, ",") + "]" 196 | } 197 | func serializeListNodeArr(arr []*ListNode) string { 198 | newArr := []string{} 199 | for i := 0; i < len(arr); i++ { 200 | newArr = append(newArr, serializeListNode(arr[i])) 201 | } 202 | return "[" + strings.Join(newArr, ",") + "]" 203 | } 204 | 205 | func parseInteger(param string) int { 206 | num, err := strconv.Atoi(param) 207 | if err != nil { 208 | panic(err) 209 | } 210 | return num 211 | } 212 | func parseString(param string) string { 213 | var r string 214 | json.Unmarshal([]byte(param), &r) 215 | return r 216 | } 217 | func parseChar(param string) byte { 218 | r := parseString(param) 219 | return r[0] 220 | } 221 | func parseFloat(param string) float64 { 222 | num, err := strconv.ParseFloat(param, 64) 223 | if err != nil { 224 | panic(err) 225 | } 226 | return num 227 | } 228 | func parseIntegerArr(param string) []int { 229 | var r []int 230 | json.Unmarshal([]byte(param), &r) 231 | return r 232 | } 233 | func parseStringArr(param string) []string { 234 | var r []string 235 | json.Unmarshal([]byte(param), &r) 236 | return r 237 | } 238 | func parseCharArr(param string) []byte { 239 | strArr := parseStringArr(param) 240 | var r []byte 241 | for i := 0; i < len(strArr); i++ { 242 | r = append(r, strArr[i][0]) 243 | } 244 | return r 245 | } 246 | func parseIntegerArrArr(param string) [][]int { 247 | var r [][]int 248 | json.Unmarshal([]byte(param), &r) 249 | return r 250 | } 251 | 252 | func parseStringArrArr(param string) [][]string { 253 | var r [][]string 254 | json.Unmarshal([]byte(param), &r) 255 | return r 256 | } 257 | func parseCharArrArr(param string) [][]byte { 258 | strArrArr := parseStringArrArr(param) 259 | var r [][]byte 260 | for i := 0; i < len(strArrArr); i++ { 261 | var item []byte 262 | for j := 0; j < len(strArrArr[i]); j++ { 263 | item = append(item, strArrArr[i][j][0]) 264 | } 265 | r = append(r, item) 266 | } 267 | return r 268 | } 269 | func serializeFloat(a float64) string { 270 | return strconv.FormatFloat(a, 'f', 5, 64) 271 | } 272 | func serializeChar(a byte) string { 273 | return serializeInterface(string(a)) 274 | } 275 | func serializeCharArr(a []byte) string { 276 | var strArr []string 277 | for i := 0; i < len(a); i++ { 278 | strArr = append(strArr, string(a[i])) 279 | } 280 | return serializeInterface(strArr) 281 | } 282 | func serializeCharArrArr(a [][]byte) string { 283 | var strArr [][]string 284 | for i := 0; i < len(a); i++ { 285 | var item []string 286 | for j := 0; j < len(a[i]); j++ { 287 | item = append(item, string(a[i][j])) 288 | } 289 | strArr = append(strArr, item) 290 | } 291 | return serializeInterface(strArr) 292 | } 293 | -------------------------------------------------------------------------------- /src/webview/submitHistory.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as vscode from 'vscode' 3 | import { api } from '../api/index' 4 | import { config } from '../config' 5 | import { getHistory, getRemoteSubmits, updateComment } from '../history' 6 | import { highlightCode } from '../util' 7 | 8 | class BuildSubmitHistoryPanel { 9 | /** 10 | * Track the currently panel. Only allow a single panel to exist at a time. 11 | */ 12 | public static currentPanel: BuildSubmitHistoryPanel | undefined 13 | public static readonly viewType = 'submitHistoryView' 14 | 15 | private readonly _panel: vscode.WebviewPanel 16 | private readonly _extensionPath: string 17 | private context: vscode.ExtensionContext 18 | public static _text: string 19 | private _disposables: vscode.Disposable[] = [] 20 | public static commands = new Map() 21 | private question_id: string 22 | 23 | public static createOrShow(context: vscode.ExtensionContext, question_id: string) { 24 | const column = vscode.ViewColumn.Two 25 | const extensionPath = context.extensionPath 26 | // If we already have a panel, show it. 27 | 28 | if (BuildSubmitHistoryPanel.currentPanel) { 29 | BuildSubmitHistoryPanel.currentPanel._panel.reveal(column) 30 | BuildSubmitHistoryPanel.currentPanel.update(question_id) 31 | return 32 | } 33 | 34 | // Otherwise, create a new panel. 35 | const panel = vscode.window.createWebviewPanel( 36 | BuildSubmitHistoryPanel.viewType, 37 | 'history', 38 | vscode.ViewColumn.Two, 39 | { 40 | // Enable javascript in the webview 41 | enableScripts: true, 42 | localResourceRoots: [vscode.Uri.file(path.join(extensionPath, 'media'))], 43 | } 44 | ) 45 | 46 | BuildSubmitHistoryPanel.currentPanel = new BuildSubmitHistoryPanel(panel, context, question_id) 47 | } 48 | 49 | private constructor(panel: vscode.WebviewPanel, context: vscode.ExtensionContext, question_id: string) { 50 | this._panel = panel 51 | this._extensionPath = context.extensionPath 52 | this.context = context 53 | this.question_id = question_id 54 | this.initView() 55 | this._update() 56 | // Listen for when the panel is disposed 57 | // This happens when the user closes the panel or when the panel is closed programatically 58 | this._panel.onDidDispose(() => this.dispose(), null, this._disposables) 59 | this._panel.webview.onDidReceiveMessage( 60 | (message) => { 61 | switch (message.command) { 62 | case 'getSubmissionCode': 63 | api.fetchSubmissionDetail({ id: message.id }) 64 | .then((code) => { 65 | const p = highlightCode(code, message.lang) 66 | console.log(p) 67 | this._panel.webview.postMessage({ 68 | command: 'submissionDetail', 69 | data: { 70 | code: highlightCode(code, message.lang), 71 | id: message.id, 72 | uuid: message.uuid, 73 | }, 74 | }) 75 | }) 76 | .catch((err) => { 77 | this._panel.webview.postMessage({ 78 | command: 'submissionDetail', 79 | data: { 80 | code: err, 81 | id: message.id, 82 | uuid: message.uuid, 83 | }, 84 | }) 85 | }) 86 | return 87 | case 'updateComment': { 88 | updateComment(message.type, message.params) 89 | .then(() => { 90 | this._panel.webview.postMessage({ 91 | command: 'updateComment', 92 | data: { 93 | code: 200, 94 | msg: 'ok', 95 | uuid: message.uuid, 96 | }, 97 | }) 98 | }) 99 | .catch((err) => { 100 | this._panel.webview.postMessage({ 101 | command: 'updateComment', 102 | data: { 103 | code: 201, 104 | msg: 'update fail', 105 | uuid: message.uuid, 106 | }, 107 | }) 108 | config.log.appendLine(err) 109 | }) 110 | } 111 | } 112 | }, 113 | null, 114 | this._disposables 115 | ) 116 | } 117 | 118 | public dispose() { 119 | BuildSubmitHistoryPanel.currentPanel = undefined 120 | 121 | // Clean up our resources 122 | this._panel.dispose() 123 | while (this._disposables.length) { 124 | const x = this._disposables.pop() 125 | if (x) { 126 | x.dispose() 127 | } 128 | } 129 | } 130 | public update(question_id: string) { 131 | if (question_id !== this.question_id) { 132 | this.question_id = question_id 133 | // BuildSubmitHistoryPanel._text = text; 134 | 135 | this._update() 136 | } 137 | } 138 | private getWebviewUri(name: string) { 139 | const webview = this._panel.webview 140 | const scriptPathOnDisk = vscode.Uri.file(path.join(this._extensionPath, 'media', name)) 141 | return webview.asWebviewUri(scriptPathOnDisk) 142 | } 143 | private initView() { 144 | const nonce = getNonce() 145 | 146 | const historyUri = this.getWebviewUri('history.js') 147 | const scriptUri = this.getWebviewUri('highlight.min.js') 148 | const cssUri = this.getWebviewUri('highlight.css') 149 | const historyCssUri = this.getWebviewUri('history.css') 150 | this._panel.webview.html = ` 151 | 152 | 153 | 154 | 158 | 159 | 160 | Cat Coding 161 | 162 | 163 | 164 | 165 | 176 |
177 |
178 | 179 | 180 | 181 | 182 | 183 | ` 184 | } 185 | private async _update() { 186 | const question_id = this.question_id 187 | const data = await getHistory(question_id, highlightCode) 188 | this._panel.webview.postMessage({ command: 'init', data: data }) 189 | getRemoteSubmits(question_id).then((data) => { 190 | this._panel.webview.postMessage({ 191 | command: 'remoteStorageData', 192 | data: data, 193 | }) 194 | }) 195 | } 196 | } 197 | 198 | function getNonce() { 199 | let text = '' 200 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 201 | for (let i = 0; i < 32; i++) { 202 | text += possible.charAt(Math.floor(Math.random() * possible.length)) 203 | } 204 | return text 205 | } 206 | 207 | export function createSubmitHistoryPanel(context: vscode.ExtensionContext, question_id: string): void { 208 | BuildSubmitHistoryPanel.createOrShow(context, question_id) 209 | } 210 | -------------------------------------------------------------------------------- /src/model/common.ts: -------------------------------------------------------------------------------- 1 | export interface GraphqlVariables { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | [key: string]: any 4 | } 5 | export interface GraphRes { 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | errors: any 8 | data: T 9 | } 10 | export enum Lang { 11 | en = 'en', 12 | cn = 'cn', 13 | } 14 | export interface Tag { 15 | slug: string 16 | name: string 17 | questions: number[] 18 | translatedName: string 19 | } 20 | export interface CodeSnippet { 21 | lang: string 22 | langSlug: string 23 | code: string 24 | } 25 | // export type CodeLang = 'C++' | 'Java' | 'Python' | 'Python3' | 'C' | 'C#' | 'JavaScript' | 'Ruby' | 'Swift' | 'Go' | 'Scala' | 'Kotlin' | 'Rust' | 'PHP' | 'TypeScript' 26 | // export const CodeLangs = ['C++', 'Java', 'Python', 'Python3', 'C', 'C#', 'JavaScript', 'Ruby', 'Swift', 'Go', 'Scala', 'Kotlin', 'Rust', 'PHP', 'TypeScript'] 27 | export interface ConciseQuestion { 28 | fid: string 29 | level: number 30 | id: number 31 | title: string 32 | slug: string 33 | acs: number 34 | submitted: number 35 | paid_only: boolean 36 | status: string 37 | name: string 38 | } 39 | export interface CheckResponse { 40 | status_code: number 41 | lang: string 42 | run_success: boolean 43 | status_runtime: string 44 | memory: number 45 | question_id: string 46 | elapsed_time: number 47 | compare_result: string 48 | code_output: string 49 | std_output: string 50 | last_testcase: string 51 | task_finish_time: number 52 | task_name: string 53 | finished: boolean 54 | status_msg: string 55 | state: State 56 | fast_submit: boolean 57 | total_correct: number 58 | total_testcases: number 59 | submission_id: string 60 | runtime_percentile: number 61 | status_memory: string 62 | memory_percentile: number 63 | pretty_lang: string 64 | input_formatted?: string 65 | expected_output?: string 66 | } 67 | type State = 'PENDING' | 'STARTED' | 'SUCCESS' 68 | export interface MapIdConciseQuestion { 69 | [id: number]: ConciseQuestion 70 | } 71 | export interface Problems { 72 | user_name: string 73 | num_solved: number 74 | num_total: number 75 | ac_easy: number 76 | ac_medium: number 77 | ac_hard: number 78 | stat_status_pairs: StatStatusPairs[] 79 | frequency_high: number 80 | frequency_mid: number 81 | category_slug: string 82 | } 83 | interface StatStatusPairs { 84 | stat: Stat 85 | status: unknown 86 | difficulty: Difficulty 87 | paid_only: boolean 88 | is_favor: boolean 89 | frequency: number 90 | progress: number 91 | } 92 | interface Difficulty { 93 | level: number 94 | } 95 | interface Stat { 96 | question_id: number 97 | question__title: string 98 | question__title_slug: string 99 | question__hide: boolean 100 | total_acs: number 101 | total_submitted: number 102 | total_column_articles: number 103 | frontend_question_id: string 104 | is_new_question: boolean 105 | } 106 | export enum ErrorStatus { 107 | Unlogin = 403, 108 | InvalidCookie = 499, 109 | } 110 | 111 | export enum AskForImportState { 112 | Yes = 'Yes', 113 | No = 'No', 114 | Later = 'Later', 115 | } 116 | 117 | export enum PendingStatus { 118 | NotPending = 'Not Pending', 119 | Pending = 'Pending', 120 | } 121 | export enum FlagType { 122 | BLUE = 'BLUE', 123 | ORANGE = 'ORANGE', 124 | GREEN = 'GREEN', 125 | PURPLE = 'PURPLE', 126 | RED = 'RED', 127 | } 128 | export interface SubmissionComment { 129 | comment: string 130 | flagType: FlagType 131 | } 132 | export interface Submission { 133 | id: string 134 | isPending: PendingStatus 135 | submissionComment?: SubmissionComment 136 | lang: string 137 | memory: string 138 | runtime: string 139 | statusDisplay: string 140 | timestamp: string 141 | url: string 142 | } 143 | 144 | export interface LocalSubmission { 145 | code: string 146 | submission: Submission 147 | } 148 | 149 | export type LocalSubmissionArr = LocalSubmission[] 150 | 151 | export enum HistoryType { 152 | Answer = 'answer', 153 | LocalSubmit = 'localSubmit', 154 | RemoteSubmit = 'remoteSubmit', 155 | } 156 | 157 | export interface UpdateCommentOption { 158 | id: string 159 | comment: string 160 | questionId: string 161 | } 162 | export type UpdateRemoteCommentOption = Omit 163 | 164 | type Requred = { 165 | [key in R]-?: T[key] 166 | } & 167 | { 168 | [key in keyof T]: T[key] 169 | } 170 | export function checkParams(obj: T, attrs: R[]): asserts obj is Requred { 171 | const verify = attrs.every((attr) => !!obj[attr]) 172 | if (!verify) { 173 | const attr = attrs.find((attr) => !obj[attr])! 174 | throw new Error(`options error, ${attr} is ${obj[attr]}`) 175 | } 176 | } 177 | 178 | export interface ContestDetail { 179 | contest: Contest 180 | questions: ContestQuestion[] 181 | user_num: number 182 | has_chosen_contact: boolean 183 | company: Company 184 | registered: boolean 185 | containsPremium: boolean 186 | } 187 | interface Company { 188 | name: string 189 | description: string 190 | logo: string 191 | slug?: string 192 | } 193 | interface ContestQuestion { 194 | id: number 195 | question_id: number 196 | credit: number 197 | title: string 198 | english_title: string 199 | title_slug: string 200 | category_slug: string 201 | } 202 | interface Contest { 203 | id: number 204 | title: string 205 | title_slug: string 206 | description: string 207 | duration: number 208 | start_time: number 209 | is_virtual: boolean 210 | origin_start_time: number 211 | is_private: boolean 212 | related_contest_title?: unknown 213 | discuss_topic_id?: number 214 | } 215 | 216 | export interface CnContestPageData { 217 | questionId: string 218 | questionIdHash: string 219 | questionTitleSlug: string 220 | questionTitle: string 221 | questionSourceTitle: string 222 | questionExampleTestcases: string 223 | categoryTitle: string 224 | contestTitleSlug: string 225 | loginUrl: string 226 | isSignedIn: boolean 227 | sessionId: string 228 | reverseUrl: ReverseUrl 229 | enableRunCode: boolean 230 | enableSubmit: boolean 231 | submitUrl: string 232 | interpretUrl: string 233 | judgeType: string 234 | nextChallengePairs?: unknown 235 | codeDefinition: CodeDefinition[] 236 | enableTestMode: boolean 237 | metaData: MetaData 238 | sampleTestCase: string 239 | judgerAvailable: boolean 240 | envInfo: EnvInfo 241 | questionContent: string 242 | questionSourceContent: string 243 | editorType: string 244 | } 245 | interface EnvInfo { 246 | cpp: string[] 247 | java: string[] 248 | python: string[] 249 | c: string[] 250 | csharp: string[] 251 | javascript: string[] 252 | ruby: string[] 253 | swift: string[] 254 | golang: string[] 255 | python3: string[] 256 | scala: string[] 257 | kotlin: string[] 258 | rust: string[] 259 | php: string[] 260 | typescript: string[] 261 | racket: string[] 262 | } 263 | interface MetaData { 264 | name: string 265 | params: Param[] 266 | return: Return 267 | } 268 | interface Return { 269 | type: string 270 | } 271 | interface Param { 272 | name: string 273 | type: string 274 | } 275 | export interface CodeDefinition { 276 | value: string 277 | text: string 278 | defaultCode: string 279 | } 280 | interface ReverseUrl { 281 | latest_submission: string 282 | account_login: string 283 | maintenance: string 284 | profile: string 285 | } 286 | 287 | export interface CommonQuestion { 288 | translatedContent: string 289 | translatedTitle: string 290 | questionFrontendId: string 291 | metaData: string 292 | content: string 293 | title: string 294 | titleSlug: string 295 | codeSnippets: CodeSnippet[] 296 | questionId: string 297 | questionSourceContent?: string 298 | } 299 | -------------------------------------------------------------------------------- /src/model/question.ts: -------------------------------------------------------------------------------- 1 | import { CodeSnippet, GraphqlVariables, SubmissionComment } from './common' 2 | 3 | export interface Chapter { 4 | descriptionText: string 5 | id: string 6 | slug: string 7 | title: string 8 | } 9 | export interface ChaptersRes { 10 | chapters: Chapter[] 11 | } 12 | interface ChapterQuestion { 13 | chapterId: number 14 | id: string 15 | isEligibleForCompletion: boolean 16 | paidOnly: boolean 17 | prerequisites: unknown[] 18 | title: string 19 | type: number 20 | } 21 | interface ChapterDetail { 22 | description: string 23 | id: string 24 | slug: string 25 | title: string 26 | items: ChapterQuestion[] 27 | } 28 | export interface ChapterRes { 29 | chapter: ChapterDetail 30 | } 31 | interface GetOrCreateExploreSession { 32 | progress: string 33 | } 34 | 35 | interface DailyProgress { 36 | is_complete: boolean 37 | } 38 | interface DailyQuestionMap { 39 | [key: string]: DailyProgress 40 | } 41 | export interface DailyWeekMap { 42 | [key: string]: DailyQuestionMap 43 | } 44 | export interface ChaptersProgressRes { 45 | getOrCreateExploreSession: GetOrCreateExploreSession 46 | } 47 | interface ChapterItem { 48 | question: Pick 49 | } 50 | export interface ChapterItemRes { 51 | item: ChapterItem 52 | } 53 | 54 | export interface ConciseQuestion { 55 | fid: string 56 | level: number 57 | id: number 58 | title: string 59 | slug: string 60 | acs: number 61 | submitted: number 62 | paid_only: boolean 63 | status: string 64 | name: string 65 | } 66 | export interface MapIdConciseQuestion { 67 | [id: number]: ConciseQuestion 68 | } 69 | 70 | export interface TodayRecordData { 71 | todayRecord: TodayRecord[] 72 | } 73 | interface TodayRecord { 74 | question: Pick 75 | lastSubmission: LastSubmission 76 | date: string 77 | userStatus: UserStatus 78 | __typename: string 79 | } 80 | interface LastSubmission { 81 | id: string 82 | __typename: string 83 | } 84 | type UserStatus = 'FINISH' | 'NOT_START' 85 | 86 | export interface QuestionTranslationData { 87 | translations: Translation[] 88 | } 89 | interface Translation { 90 | questionId: string 91 | title: string 92 | } 93 | 94 | export interface GraphqlResponse { 95 | data: T 96 | } 97 | export interface DailyQuestionRecordData { 98 | dailyQuestionRecords: [DailyQuestionRecord] 99 | } 100 | export interface DailyQuestionRecord { 101 | date: string 102 | 103 | question: Pick 104 | userStatus: string 105 | __typename: string 106 | } 107 | 108 | export interface GraphqlRequestData { 109 | operationName: string | null 110 | query: string 111 | variables: GraphqlVariables 112 | } 113 | 114 | export interface QuestionData { 115 | question: Question 116 | } 117 | export interface Question { 118 | questionId: string 119 | questionFrontendId: string 120 | boundTopicId: number 121 | title: string 122 | titleSlug: string 123 | content: string 124 | translatedTitle: string 125 | translatedContent: string 126 | isPaidOnly: boolean 127 | difficulty: string 128 | likes: number 129 | dislikes: number 130 | isLiked: unknown 131 | similarQuestions: string 132 | contributors: [] 133 | langToValidPlayground: string 134 | topicTags: TopicTags[] 135 | companyTagStats: unknown 136 | codeSnippets: CodeSnippet[] 137 | stats: string 138 | hints: string[] 139 | solution: Solution 140 | status: Status 141 | sampleTestCase: string 142 | metaData: string 143 | judgerAvailable: boolean 144 | judgeType: string 145 | mysqlSchemas: string[] 146 | enableRunCode: boolean 147 | envInfo: string 148 | book: unknown 149 | isSubscribed: boolean 150 | isDailyQuestion: boolean 151 | dailyRecordStatus: string 152 | editorType: string 153 | ugcQuestionId: unknown 154 | style: string 155 | __typename: string 156 | 157 | // questionTitleSlug: string 158 | } 159 | interface Solution { 160 | canSeeDetail: boolean 161 | id: string 162 | } 163 | type Status = 'ac' | 'notac' | null 164 | 165 | interface TopicTags { 166 | name: string 167 | slug: string 168 | translatedName: string 169 | questions: number[] //question id 170 | __typename: string 171 | } 172 | 173 | export interface ContestData { 174 | allContests: Pick[] 175 | } 176 | interface Contest { 177 | containsPremium: boolean 178 | title: string 179 | cardImg: string 180 | titleSlug: string 181 | description: string 182 | startTime: number 183 | duration: number 184 | originStartTime: number 185 | isVirtual: boolean 186 | company: Company 187 | __typename: string 188 | } 189 | interface Company { 190 | watermark: string 191 | __typename: string 192 | } 193 | 194 | export interface TagData { 195 | topics: TopicTags[] 196 | } 197 | // interface Topics { 198 | // slug: string 199 | // name: string 200 | // questions: number[] 201 | // translatedName: string 202 | // } 203 | 204 | export interface SubmitOptions { 205 | titleSlug: string 206 | typed_code: string 207 | question_id: string 208 | lang?: string 209 | } 210 | export interface SubmitContestOptions extends SubmitOptions { 211 | weekname: string 212 | } 213 | export interface SubmitResponse { 214 | submission_id: number 215 | } 216 | export interface CheckResponse { 217 | status_code: number 218 | lang: string 219 | run_success: boolean 220 | status_runtime: string 221 | memory: number 222 | question_id: string 223 | elapsed_time: number 224 | compare_result: string 225 | code_output: string 226 | std_output: string 227 | last_testcase: string 228 | task_finish_time: number 229 | task_name: string 230 | finished: boolean 231 | status_msg: string 232 | state: State 233 | fast_submit: boolean 234 | total_correct: number 235 | total_testcases: number 236 | submission_id: string 237 | runtime_percentile: number 238 | status_memory: string 239 | memory_percentile: number 240 | pretty_lang: string 241 | } 242 | 243 | export interface CheckContestResponse { 244 | state: State 245 | } 246 | type State = 'PENDING' | 'STARTED' | 'SUCCESS' 247 | 248 | export interface CheckOptions { 249 | submission_id: number 250 | titleSlug: string 251 | } 252 | export interface CheckContestOptions extends CheckOptions { 253 | weekname: string 254 | } 255 | 256 | export interface SubmissionsResponse { 257 | submissionList: SubmissionList 258 | } 259 | export interface SubmissionsOptions { 260 | lastKey?: string 261 | limit?: number 262 | offset?: number 263 | titleSlug: string 264 | } 265 | interface SubmissionList { 266 | lastKey: string 267 | hasNext: boolean 268 | submissions: Submissions[] 269 | } 270 | export interface Submissions { 271 | id: string 272 | statusDisplay: string 273 | lang: string 274 | runtime: string 275 | timestamp: string 276 | url: string 277 | isPending: string 278 | memory: string 279 | submissionComment?: SubmissionComment 280 | } 281 | export interface SubmissionDetailOptions { 282 | id: string 283 | } 284 | 285 | export interface SubmissionDetailPageData { 286 | submissionData: SubmissionData 287 | questionId: string 288 | sessionId: string 289 | getLangDisplay: string 290 | submissionCode: string 291 | editCodeUrl: string 292 | checkUrl: string 293 | runtimeDistributionFormatted: string 294 | memoryDistributionFormatted: string 295 | langs: unknown[] 296 | runtime: string 297 | memory: string 298 | enableMemoryDistribution: string 299 | nonSufficientMsg: string 300 | } 301 | interface SubmissionData { 302 | status_code: number 303 | runtime: string 304 | memory: string 305 | total_correct: string 306 | total_testcases: string 307 | compare_result: string 308 | input_formatted: string 309 | input: string 310 | expected_output: string 311 | code_output: string 312 | last_testcase: string 313 | } 314 | -------------------------------------------------------------------------------- /src/common/parseContent.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'pretty-object-string' 2 | import { escape2html } from './util' 3 | const isSpace = (c) => /\s/.test(c) 4 | const isWord = (c) => /\w/.test(c) 5 | function delComment(src) { 6 | const commentRegExp = /(\/\*([\s\S]*?)\*\/|('.*?')|(".*?")|\/\/(.*)$)/gm 7 | return src.replace(commentRegExp, commentReplace) 8 | } 9 | function commentReplace(_match, _multi, _multiText, singlePrefix, double) { 10 | return singlePrefix || double || '' 11 | } 12 | interface Demo { 13 | input: Input[] 14 | output: string 15 | } 16 | enum Kind { 17 | scanContinue, 18 | scanBeginTag, 19 | scanTagText, 20 | scanEndTag, 21 | scanWord, 22 | scanSpace, 23 | scanInputFlag, 24 | Identify, 25 | scanEqual, 26 | scanParam, 27 | scanResult, 28 | } 29 | interface Input { 30 | key?: string 31 | value?: string 32 | } 33 | //TODO refactor 34 | export class ParseContent { 35 | public step: (char: string, next: string, i: number) => Kind = this.stateBegin 36 | public prevStep: (char: string, next?: string) => Kind = this.stateBegin 37 | public word = '' 38 | public words: string[] = [] 39 | public tagStatus = 0 40 | public exampleState = 0 41 | public demos: Demo[] = [] 42 | constructor(public readonly content: string) { 43 | this.content = delComment(escape2html(content.replace(/<[^>]*>/g, ''))) 44 | this.init() 45 | } 46 | init() { 47 | this.step = this.stateBegin 48 | let input: Input[] = [] 49 | const linkReg = /^\s*-?\d+->/ 50 | const demos: Demo[] = [] 51 | let identify 52 | let i = 0 53 | try { 54 | for (; i < this.content.length; i++) { 55 | const c = this.content[i] 56 | const n = this.content[i + 1] 57 | 58 | const out = this.step(c, n, i) 59 | if (out === Kind.Identify) { 60 | identify = this.word 61 | this.word = '' 62 | } else if (out === Kind.scanParam) { 63 | let value = this.content.slice(i, i + 10) 64 | let index, output 65 | if (linkReg.test(value)) { 66 | const result = this.parseLink(i) 67 | index = result.index 68 | output = result.output 69 | } else { 70 | value = this.content.slice(i) 71 | const result = parse(value, { 72 | partialIndex: true, 73 | compress: true, 74 | }) 75 | index = result.index 76 | output = result.output 77 | } 78 | input.push({ 79 | key: identify, 80 | value: output, 81 | }) 82 | 83 | i = i + index - 1 84 | this.step = this.stateInputIdentity2 85 | } else if (out === Kind.scanResult) { 86 | let value = this.content.slice(i, i + 10) 87 | let index, output 88 | if (linkReg.test(value)) { 89 | const result = this.parseLink(i) 90 | index = result.index 91 | output = result.output 92 | } else { 93 | value = this.content.slice(i) 94 | const result = parse(value, { 95 | partialIndex: true, 96 | compress: true, 97 | }) 98 | index = result.index 99 | output = result.output 100 | } 101 | i = i + index - 1 102 | demos.push({ 103 | input, 104 | output: output, 105 | }) 106 | input = [] 107 | this.word = '' 108 | identify = '' 109 | this.step = this.stateWord 110 | } 111 | } 112 | } catch (err) { 113 | console.log('content', this.content) 114 | console.log(this.content.slice(i - 20, i)) 115 | console.log(err) 116 | } 117 | this.demos = demos 118 | } 119 | static getTestCases(content: string) { 120 | const p = new ParseContent(content) 121 | return p.demos 122 | } 123 | parseLink(index) { 124 | let numStr = '' 125 | const output: number[] = [] 126 | const start = index 127 | 128 | while (index < this.content.length && isSpace(this.content[index])) { 129 | index++ 130 | } 131 | while (index < this.content.length && /[\d->N]/.test(this.content[index])) { 132 | const char = this.content[index] 133 | if (/\d/.test(char)) { 134 | numStr += char 135 | } else if (char === '-') { 136 | const nextChar = this.content[index + 1] 137 | if (nextChar === '>') { 138 | output.push(parseInt(numStr)) 139 | numStr = '' 140 | } else if (/\d/.test(nextChar)) { 141 | numStr = char 142 | } 143 | } else if (char === 'N') { 144 | if (this.content.slice(index, index + 4) !== 'NULL') { 145 | throw new Error('parse link error') 146 | } 147 | return { 148 | index: index + 4 - start, 149 | output: JSON.stringify(output), 150 | } 151 | } 152 | index++ 153 | } 154 | output.push(parseInt(numStr)) 155 | return { 156 | index: index - start, 157 | output: JSON.stringify(output), 158 | } 159 | } 160 | lineEndAt(position) { 161 | let i = position 162 | while (this.content[i++]) { 163 | if (this.content[i] === '\n') { 164 | return i 165 | } 166 | } 167 | return i 168 | } 169 | stateBegin(char: string): Kind { 170 | if (isSpace(char)) { 171 | return Kind.scanContinue 172 | } 173 | 174 | this.step = this.stateWord 175 | return this.stateWord(char) 176 | } 177 | stateWord(char: string): Kind { 178 | if (isSpace(char)) { 179 | this.word = '' 180 | return Kind.scanSpace 181 | } 182 | this.word += char 183 | if (this.exampleState === 1 && this.word === 'Input:') { 184 | this.word = '' 185 | this.exampleState = 0 186 | this.step = this.stateInputIdentityOrValue 187 | } else if (this.word === 'Example') { 188 | this.exampleState = 1 189 | } 190 | return Kind.scanWord 191 | } 192 | stateInputIdentityOrValue(char: string, n: string, i: number): Kind { 193 | if (isSpace(char)) { 194 | return Kind.scanSpace 195 | } 196 | if (!/[a-zA-Z_]/.test(char) || /(true|false|null)/.test(this.content.slice(i, i + 6))) { 197 | return Kind.scanParam 198 | } 199 | if (this.content.slice(i, i + 7) === 'Output:') { 200 | //Compatibility Special Conditions id:53 201 | /** 202 | * content Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum. 203 | 204 | Example: 205 | 206 | 207 | Input: [-2,1,-3,4,-1,2,1,-5,4], 208 | Output: 6 209 | Explanation: [4,-1,2,1] has the largest sum = 6. 210 | 211 | */ 212 | this.step = this.stateOut 213 | return this.step(char, n, i) 214 | } 215 | this.step = this.stateInputIdentity 216 | return this.step(char, n, i) 217 | } 218 | stateInputIdentity(char: string, _n: string, _i: number): Kind { 219 | if (isSpace(char)) { 220 | if (!this.word) { 221 | return Kind.scanSpace 222 | } 223 | this.step = this.stateEqual 224 | return Kind.Identify 225 | } 226 | if (!isWord(char)) { 227 | throw new Error('input identity invalid') 228 | } 229 | this.word += char 230 | return Kind.scanWord 231 | } 232 | stateEqual(char: string): Kind { 233 | if (isSpace(char)) { 234 | return Kind.scanSpace 235 | } 236 | if (char === '=') { 237 | this.step = this.stateParam 238 | return Kind.scanEqual 239 | } 240 | throw new Error('parse equal error') 241 | } 242 | stateParam(char: string): Kind { 243 | if (isSpace(char)) { 244 | return Kind.scanSpace 245 | } 246 | return Kind.scanParam 247 | } 248 | stateInputIdentity2(char: string, n: string, i: number): Kind { 249 | if (isSpace(char)) { 250 | return Kind.scanSpace 251 | } 252 | if (char === ',') { 253 | this.step = this.stateInputIdentityOrValue 254 | return Kind.scanContinue 255 | } 256 | this.step = this.stateOut 257 | return this.step(char, n, i) 258 | } 259 | stateOut(char: string): Kind { 260 | if (isSpace(char)) { 261 | return Kind.scanSpace 262 | } 263 | this.word += char 264 | if (this.word === 'Output:') { 265 | this.word = '' 266 | this.step = this.stateResult 267 | return Kind.scanContinue 268 | } 269 | return Kind.scanContinue 270 | } 271 | stateResult(char: string): Kind { 272 | this.word += char 273 | return Kind.scanResult 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/fileStorage/db.ts: -------------------------------------------------------------------------------- 1 | import path = require('path') 2 | import fs = require('fs') 3 | import util = require('util') 4 | import { crc32, md5 } from './util' 5 | import { existDir, existFile, writeFileAsync } from '../common/util' 6 | const openAsync = util.promisify(fs.open) 7 | const readAsync = util.promisify(fs.read) 8 | const writeAsync = util.promisify(fs.write) 9 | const readFileAsync = util.promisify(fs.readFile) 10 | 11 | const META = 'Meta' 12 | const MAGIC = 0x125e591 13 | const VERSION = 1 14 | const HeaderConfig: Field[] = [ 15 | { 16 | field: 'magic', 17 | type: 'int', 18 | len: 32, 19 | }, 20 | { 21 | field: 'version', 22 | type: 'int', 23 | len: 32, 24 | }, 25 | { 26 | field: 'checksum', 27 | type: 'int', 28 | len: 32, 29 | }, 30 | { 31 | field: 'count', 32 | type: 'int', 33 | len: 32, 34 | }, 35 | ] 36 | const SectionHeaderConfig: Field[] = [ 37 | { 38 | field: 'checksum', 39 | type: 'int', 40 | len: 32, 41 | }, 42 | { 43 | field: 'id', 44 | type: 'int', 45 | len: 32, 46 | }, 47 | { 48 | field: 'fid', 49 | type: 'int', 50 | len: 32, 51 | }, 52 | { 53 | field: 'offset', 54 | type: 'int', 55 | len: 32, 56 | }, 57 | { 58 | field: 'size', 59 | type: 'int', 60 | len: 16, 61 | }, 62 | { 63 | field: 'hash', 64 | type: 'hex', 65 | len: 128, 66 | }, 67 | { 68 | field: 'timestamp', 69 | type: 'int', 70 | len: 64, 71 | }, 72 | ] 73 | interface DbOptions { 74 | dbDir: string 75 | filename: string 76 | } 77 | export default class Db { 78 | private dbDir: string 79 | private filename: string 80 | private headerPath: string 81 | private filePath: string 82 | public fds: number[] = [-1, -1] 83 | public buffer: Buffer = Buffer.alloc(0) 84 | public header: Header | null = null 85 | public sectionHeaders: SectionHeader[] = [] 86 | // public headerFd: number 87 | constructor(options: DbOptions) { 88 | this.dbDir = options.dbDir 89 | this.filename = options.filename 90 | this.headerPath = path.join(this.dbDir, this.filename + META) 91 | this.filePath = path.join(this.dbDir, this.filename) 92 | } 93 | async init(): Promise { 94 | await existDir(this.dbDir) 95 | await existFile(this.headerPath) 96 | await existFile(this.filePath) 97 | const headerFd = await openAsync(this.headerPath, 'r+') 98 | const dataFd = await openAsync(this.filePath, 'r+') 99 | this.fds = [headerFd, dataFd] 100 | 101 | const buffer = await readFileAsync(this.headerPath) 102 | this.buffer = buffer 103 | await this._init() 104 | } 105 | async _init(): Promise { 106 | const buffer = this.buffer 107 | let header: Header 108 | const sectionHeaders: SectionHeader[] = [] 109 | if (!buffer.length) { 110 | const count = 0 111 | header = { 112 | magic: MAGIC, 113 | version: VERSION, 114 | checksum: crc32(Buffer.alloc(4)), 115 | count, 116 | } 117 | await writeFileAsync(this.headerPath, convertToBuffer(header, HeaderConfig)) 118 | } else { 119 | const headerLen = getLen(HeaderConfig) 120 | const headerBuf = buffer.slice(0, headerLen) 121 | header = convertToObj
(headerBuf, HeaderConfig) 122 | 123 | // header = { 124 | // magic: buffer.slice(0, 4).readInt32BE(), 125 | // version: buffer.slice(4, 8).readInt32BE(), 126 | // checksum: buffer.slice(8, 12).readInt32BE(), 127 | // count: buffer.slice(12, 16).readInt32BE() 128 | // } 129 | const sectionHeadersBuf = buffer.slice(headerLen) 130 | const len = getLen(SectionHeaderConfig) 131 | for (let i = 0; i < sectionHeadersBuf.length / len; i++) { 132 | const buf = sectionHeadersBuf.slice(i * len, (i + 1) * len) 133 | 134 | sectionHeaders.push(convertToObj(buf, SectionHeaderConfig)) 135 | } 136 | } 137 | this.header = header 138 | this.sectionHeaders = sectionHeaders 139 | } 140 | async read(id: number): Promise { 141 | const sectionHeader = this.sectionHeaders.find((v) => v.id === id) 142 | if (sectionHeader) { 143 | const buf = Buffer.alloc(sectionHeader.size) 144 | await readAsync(this.fds[1], buf, 0, buf.length, sectionHeader.offset) 145 | return buf.toString('utf8') 146 | } 147 | return '' 148 | } 149 | async add(question: string, id: number, fid: number): Promise { 150 | if (this.sectionHeaders.find((v) => v.id === id)) { 151 | return 152 | } 153 | const buffer = Buffer.from(question) 154 | const count = (this.header as Header).count 155 | const last = count > 0 ? this.sectionHeaders[count - 1] : { offset: 0, size: 0 } 156 | const offset = last.offset + last.size 157 | const size = buffer.length 158 | const hash = md5(buffer) 159 | const partialSecHeader = { 160 | id, 161 | fid, 162 | offset, 163 | size, 164 | hash, 165 | timestamp: Date.now(), 166 | } 167 | const config = SectionHeaderConfig.slice(1) 168 | const partialSecHeaderBuf = convertToBuffer(partialSecHeader, config) 169 | const checksum = crc32(partialSecHeaderBuf) 170 | const sectionHeader = { 171 | checksum, 172 | ...partialSecHeader, 173 | } 174 | this.sectionHeaders[count] = sectionHeader 175 | 176 | const secHeaderBuf = convertToBuffer(sectionHeader, SectionHeaderConfig) 177 | 178 | ;(this.header as Header).count++ 179 | ;(this.header as Header).checksum = crc32(convertToBuffer(this.sectionHeaders, SectionHeaderConfig)) 180 | const headerBuf = convertToBuffer(this.header, HeaderConfig) 181 | const headerLen = getLen(HeaderConfig) 182 | const secHeaderLen = getLen(SectionHeaderConfig) 183 | const secOffset = headerLen + count * secHeaderLen 184 | await Promise.all([ 185 | writeAsync(this.fds[0], secHeaderBuf, 0, secHeaderBuf.length, secOffset), 186 | writeAsync(this.fds[0], headerBuf, 0, headerBuf.length, 0), 187 | writeAsync(this.fds[1], buffer, 0, buffer.length, offset), 188 | ]) 189 | } 190 | } 191 | interface Header { 192 | magic: number 193 | version: number 194 | checksum: number 195 | count: number 196 | } 197 | interface SectionHeader { 198 | checksum: number 199 | id: number 200 | fid: number 201 | offset: number 202 | size: number 203 | hash: string 204 | timestamp: number 205 | } 206 | type FieldType = 'int' | 'hex' 207 | export interface Field { 208 | field: string 209 | type: FieldType 210 | len: number 211 | } 212 | 213 | export function convertToObj(buffer: Buffer, config: Field[]): T { 214 | let i = 0 215 | const obj: Partial = {} 216 | for (const field of config) { 217 | obj[field.field] = read(buffer, i, field) 218 | i += field.len / 8 219 | } 220 | return obj as T 221 | } 222 | export function convertToBuffer(obj: T[] | T, config: Field[]): Buffer { 223 | if (Array.isArray(obj)) { 224 | return Buffer.concat(obj.map((v) => convertToBuffer(v, config))) 225 | } 226 | let i = 0 227 | const len = getLen(config) 228 | const buf = Buffer.alloc(len) 229 | 230 | for (const field of config) { 231 | write(obj[field.field], buf, i, field) 232 | i += field.len / 8 233 | } 234 | return buf 235 | } 236 | 237 | function getLen(config: Field[]) { 238 | return config.reduce((prev, cur) => prev + cur.len / 8, 0) 239 | } 240 | 241 | function read(buffer: Buffer, offset: number, field: Field) { 242 | const mapFun = { 243 | int: (buffer: Buffer) => { 244 | const byteLen = field.len / 8 245 | if (byteLen === 8) { 246 | try { 247 | const a = Number(buffer.readBigUInt64BE(offset)) 248 | return a 249 | } catch (err) { 250 | console.log(buffer) 251 | } 252 | } else { 253 | return buffer.readUIntBE(offset, field.len / 8) 254 | } 255 | }, 256 | 257 | hex: (buffer: Buffer) => buffer.slice(offset, offset + field.len / 8).toString('hex'), 258 | } 259 | const fun = mapFun[field.type] 260 | return fun(buffer) 261 | } 262 | function write(value: number | string, buffer: Buffer, offset: number, field: Field) { 263 | const mapFunc = { 264 | int: () => { 265 | const byteLen = field.len / 8 266 | if (byteLen === 8) { 267 | buffer.writeBigInt64BE(BigInt(value as number), offset) 268 | } else { 269 | buffer.writeUIntBE(value as number, offset, field.len / 8) 270 | } 271 | }, 272 | hex: () => Buffer.from(value as string, 'hex').copy(buffer, offset), 273 | } 274 | const fun = mapFunc[field.type] 275 | return fun() 276 | } 277 | --------------------------------------------------------------------------------