()
+ input.title = title
+ input.step = step
+ input.totalSteps = totalSteps
+ input.placeholder = placeholder
+ input.items = items
+ if (activeItem) {
+ input.activeItems = [activeItem]
+ }
+ input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])]
+ disposables.push(
+ input.onDidTriggerButton((item) => {
+ if (item === QuickInputButtons.Back) {
+ reject(InputFlowAction.back)
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ resolve(item as any)
+ }
+ }),
+ input.onDidChangeSelection((items) => resolve(items[0])),
+ input.onDidHide(() => {
+ ; (async () => {
+ reject(
+ shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel
+ )
+ })().catch(reject)
+ })
+ )
+ if (this.current) {
+ this.current.dispose()
+ }
+ this.current = input
+ this.current.show()
+ })
+ } finally {
+ disposables.forEach((d) => d.dispose())
+ }
+ }
+
+ async showInputBox({
+ title,
+ step,
+ totalSteps,
+ password = false,
+ value,
+ prompt,
+ validate,
+ buttons,
+ shouldResume,
+ }: P) {
+ const disposables: Disposable[] = []
+ try {
+ return await new Promise((resolve, reject) => {
+ const input = window.createInputBox()
+ input.title = title
+ input.step = step
+ input.totalSteps = totalSteps
+ input.value = value || ''
+ input.prompt = prompt
+ input.password = password
+ input.buttons = [...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), ...(buttons || [])]
+ let validating = validate('')
+ disposables.push(
+ input.onDidTriggerButton((item) => {
+ if (item === QuickInputButtons.Back) {
+ reject(InputFlowAction.back)
+ } else {
+ resolve(item as never)
+ }
+ }),
+ input.onDidAccept(async () => {
+ const value = input.value
+ input.enabled = false
+ input.busy = true
+ if (!(await validate(value))) {
+ resolve(value)
+ }
+ input.enabled = true
+ input.busy = false
+ }),
+ input.onDidChangeValue(async (text) => {
+ const current = validate(text)
+ validating = current
+ const validationMessage = await current
+ if (current === validating) {
+ input.validationMessage = validationMessage
+ }
+ }),
+ input.onDidHide(() => {
+ ; (async () => {
+ reject(
+ shouldResume && (await shouldResume()) ? InputFlowAction.resume : InputFlowAction.cancel
+ )
+ })().catch(reject)
+ })
+ )
+ if (this.current) {
+ this.current.dispose()
+ }
+ this.current = input
+ this.current.show()
+ })
+ } finally {
+ disposables.forEach((d) => d.dispose())
+ }
+ }
+}
+export interface User {
+ name: string
+ password: string
+}
+async function inputName(input: MultiStepInput, user: Partial, title: string): Promise {
+ user.name = await input.showInputBox({
+ step: 1,
+ totalSteps: 2,
+ validate: async () => '',
+ prompt: 'please input username or email',
+ title,
+ value: '',
+ shouldResume: () => Promise.resolve(false),
+ })
+ return (input) => inputPass(input, user, title)
+}
+async function inputPass(input: MultiStepInput, user: Partial, title: string) {
+ user.password = await input.showInputBox({
+ step: 2,
+ totalSteps: 2,
+ password: true,
+ validate: async () => '',
+ prompt: 'please input password',
+ title,
+ value: '',
+ shouldResume: () => Promise.resolve(false),
+ })
+}
+function validUser(user: Partial): user is User {
+ return !!(user.name && user.password)
+}
+function validCookie(cookie: string) {
+ return cookie.includes('LEETCODE_SESSION') && cookie.includes('csrftoken')
+}
+async function refresh(questionsProvider: QuestionsProvider) {
+ cache.removeCache()
+ const refreshPromise = refreshQuestions()
+ await execWithProgress(refreshPromise, 'refresh questions')
+ questionsProvider.refresh()
+}
+export async function selectLogin(questionsProvider: QuestionsProvider) {
+ const githubItem = {
+ label: 'Third-Party:GitHub',
+ detail: 'Use GitHub account to login',
+ }
+ const cookieItem = {
+ label: 'Leetcode Cookie',
+ detail: 'Use Leetcode cookie copied from browser to login',
+ }
+ const accountItem = {
+ label: 'Leetcode Account',
+ detail: 'use Leetcode account to login',
+ }
+ const items = [githubItem, cookieItem]
+ if (config.lang === 'cn') {
+ items.unshift(accountItem)
+ }
+ const result = await window.showQuickPick(items)
+ if (result === githubItem) {
+ const user = await githubInput()
+ if (!validUser(user)) {
+ return
+ }
+ await execWithProgress(githubLogin(user), 'wait login')
+ window.showInformationMessage('login success')
+ } else if (result === cookieItem) {
+ const cookie = await cookieInput()
+ if (!cookie) {
+ return
+ }
+ if (!validCookie(cookie)) {
+ window.showErrorMessage('cookie is invalid')
+ return
+ }
+ await cookieLogin(cookie)
+ config.log.appendLine('save cookie success')
+ // window.showInformationMessage('save cookie success')
+ await refresh(questionsProvider)
+ return
+ } else if (result === accountItem) {
+ const user = await accountInput()
+ if (!validUser(user)) {
+ return
+ }
+ await accountLogin(user)
+ config.log.appendLine('login success')
+ // window.showInformationMessage('login success')
+ await refresh(questionsProvider)
+ }
+}
+export async function githubInput(): Promise> {
+ const user: Partial = {}
+ await MultiStepInput.run((input) => inputName(input, user, 'login github'))
+ return user
+}
+export async function cookieInput(): Promise {
+ const cookie = await window.showInputBox({
+ prompt: 'copy the cookie from browser',
+ })
+ return cookie
+}
+export async function accountInput() {
+ const user: Partial = {}
+ await MultiStepInput.run((input) => inputName(input, user, 'account login'))
+ return user
+}
+
+// copy from https://github.com/microsoft/vscode-extension-samples/blob/master/quickinput-sample/src/multiStepInput.ts
diff --git a/src/memo/index.ts b/src/memo/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83058ccf8fb0b0a8d2d6c2dac419150e9b268da1
--- /dev/null
+++ b/src/memo/index.ts
@@ -0,0 +1,61 @@
+import { existDir, writeFileAsync } from '../common/util'
+import { config, updateEnv } from '../config'
+import * as path from 'path'
+import { ResolverParam } from '../provider/resolver'
+export function addFolder(name: string) {
+ if (config.env.memo.find((v) => v.name === name)) {
+ throw new Error('folder already exists')
+ }
+ config.env.memo.push({
+ name,
+ children: [],
+ })
+ updateEnv('memo', config.env.memo)
+}
+export function deleteFolder(name: string) {
+ const memo = config.env.memo
+ const index = memo.findIndex((item) => item.name === name)
+ memo.splice(index, 1)
+ updateEnv('memo', memo)
+}
+export async function addFolderFile(folderName: string, fileName: string) {
+ const memo = config.env.memo
+ const folder = memo.find((item) => item.name === folderName)
+ if (folder) {
+ folder.children.push({
+ type: 'other',
+ name: fileName,
+ param: fileName,
+ })
+ const memoDir = path.join(config.cacheBaseDir, 'memo')
+ const filePath = path.join(memoDir, fileName)
+ await existDir(memoDir)
+ await writeFileAsync(filePath, Buffer.alloc(0))
+ }
+}
+export function addQuestion(folderName: string, name: string, param: Partial) {
+ const memo = config.env.memo
+ const folder = memo.find((item) => item.name === folderName)
+ if (folder) {
+ if (folder.children.find((v) => v.name === name)) {
+ throw new Error('file already exists')
+ }
+ folder.children.push({
+ type: 'question',
+ name,
+ param,
+ })
+ updateEnv('memo', memo)
+ }
+}
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+async function deleteFile(folderName: string, fileName: string) {
+ const memo = config.env.memo
+ const folder = memo.find((item) => item.name === folderName)
+ if (folder) {
+ const index = folder.children.findIndex((c) => c.name === fileName)
+ if (index !== -1) {
+ folder.children.splice(index, 1)
+ }
+ }
+}
diff --git a/src/model/command.ts b/src/model/command.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e0d6e245f64f0d4c7bf5bafe414eb36d2c0d4c19
--- /dev/null
+++ b/src/model/command.ts
@@ -0,0 +1,26 @@
+import * as vscode from 'vscode'
+import { TestCaseParam } from '../common/util'
+import { QuestionsProvider } from '../provider/questionsProvider'
+
+export interface CodeLensesOptions {
+ text: string
+ filePath: string
+ langSlug: string
+}
+export interface TestCodeLensesOptions {
+ filePath: string
+ testCaseParam: TestCaseParam
+}
+export type BuildCodeLensesOptions = CodeLensesOptions
+export type SubmitCodeLensesOptions = CodeLensesOptions
+export type GetDescriptionCodeLensesOptions = CodeLensesOptions
+export type ViewSubmitHistoryCodeLensesOptions = CodeLensesOptions
+type FilePath = string
+export type DebugLensesOptions = FilePath
+type ExtensionPath = string
+export type BuildCodeArguments = [vscode.ExtensionContext, BuildCodeLensesOptions]
+export type TestCodeArguments = [TestCodeLensesOptions]
+export type DebugCodeArguments = [DebugLensesOptions]
+export type SubmitCodeArguments = [QuestionsProvider, SubmitCodeLensesOptions]
+export type GetDescriptionArguments = [ExtensionPath, GetDescriptionCodeLensesOptions]
+export type ViewSubmitHistoryArguments = [vscode.ExtensionContext, CodeLensesOptions]
diff --git a/src/model/common.ts b/src/model/common.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8e31cfd411b62a27e92d32d3eb34a7e261902c59
--- /dev/null
+++ b/src/model/common.ts
@@ -0,0 +1,304 @@
+export interface GraphqlVariables {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [key: string]: any
+}
+export interface GraphRes {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ errors: any
+ data: T
+}
+export enum Lang {
+ en = 'en',
+ cn = 'cn',
+}
+export interface Tag {
+ slug: string
+ name: string
+ questions: number[]
+ translatedName: string
+}
+export interface Favorites {
+ id: string
+ name: string
+ questions: number[]
+ type: string
+}
+export interface CodeSnippet {
+ lang: string
+ langSlug: string
+ code: string
+}
+// export type CodeLang = 'C++' | 'Java' | 'Python' | 'Python3' | 'C' | 'C#' | 'JavaScript' | 'Ruby' | 'Swift' | 'Go' | 'Scala' | 'Kotlin' | 'Rust' | 'PHP' | 'TypeScript'
+// export const CodeLangs = ['C++', 'Java', 'Python', 'Python3', 'C', 'C#', 'JavaScript', 'Ruby', 'Swift', 'Go', 'Scala', 'Kotlin', 'Rust', 'PHP', 'TypeScript']
+export interface ConciseQuestion {
+ fid: string
+ level: number
+ id: number
+ title: string
+ slug: string
+ acs: number
+ submitted: number
+ paid_only: boolean
+ status: string
+ name: string
+}
+export interface CheckResponse {
+ status_code: number
+ lang: string
+ run_success: boolean
+ status_runtime: string
+ memory: number
+ question_id: string
+ elapsed_time: number
+ compare_result: string
+ code_output: string
+ std_output: string
+ last_testcase: string
+ task_finish_time: number
+ task_name: string
+ finished: boolean
+ status_msg: string
+ state: State
+ fast_submit: boolean
+ total_correct: number
+ total_testcases: number
+ submission_id: string
+ runtime_percentile: number
+ status_memory: string
+ memory_percentile: number
+ pretty_lang: string
+ input_formatted?: string
+ expected_output?: string
+}
+type State = 'PENDING' | 'STARTED' | 'SUCCESS'
+export interface MapIdConciseQuestion {
+ [id: number]: ConciseQuestion
+}
+export interface Problems {
+ user_name: string
+ num_solved: number
+ num_total: number
+ ac_easy: number
+ ac_medium: number
+ ac_hard: number
+ stat_status_pairs: StatStatusPairs[]
+ frequency_high: number
+ frequency_mid: number
+ category_slug: string
+}
+interface StatStatusPairs {
+ stat: Stat
+ status: unknown
+ difficulty: Difficulty
+ paid_only: boolean
+ is_favor: boolean
+ frequency: number
+ progress: number
+}
+interface Difficulty {
+ level: number
+}
+interface Stat {
+ question_id: number
+ question__title: string
+ question__title_slug: string
+ question__hide: boolean
+ total_acs: number
+ total_submitted: number
+ total_column_articles: number
+ frontend_question_id: string
+ is_new_question: boolean
+}
+export enum ErrorStatus {
+ Unlogin = 403,
+ InvalidCookie = 499,
+}
+
+export enum AskForImportState {
+ Yes = 'Yes',
+ No = 'No',
+ Later = 'Later',
+}
+
+export enum PendingStatus {
+ NotPending = 'Not Pending',
+ Pending = 'Pending',
+}
+export enum FlagType {
+ BLUE = 'BLUE',
+ ORANGE = 'ORANGE',
+ GREEN = 'GREEN',
+ PURPLE = 'PURPLE',
+ RED = 'RED',
+}
+export interface SubmissionComment {
+ comment: string
+ flagType: FlagType
+}
+export interface Submission {
+ id: string
+ isPending: PendingStatus
+ submissionComment?: SubmissionComment
+ lang: string
+ memory: string
+ runtime: string
+ statusDisplay: string
+ timestamp: string
+ url: string
+}
+
+export interface LocalSubmission {
+ code: string
+ submission: Submission
+}
+
+export type LocalSubmissionArr = LocalSubmission[]
+
+export enum HistoryType {
+ Answer = 'answer',
+ LocalSubmit = 'localSubmit',
+ RemoteSubmit = 'remoteSubmit',
+}
+
+export interface UpdateCommentOption {
+ id: string
+ comment: string
+ questionId: string
+}
+export type UpdateRemoteCommentOption = Omit
+
+type Requred = {
+ [key in R]-?: T[key]
+} &
+ {
+ [key in keyof T]: T[key]
+ }
+export function checkParams(obj: T, attrs: R[]): asserts obj is Requred {
+ const verify = attrs.every((attr) => !!obj[attr])
+ if (!verify) {
+ const attr = attrs.find((attr) => !obj[attr])!
+ throw new Error(`options error, ${String(attr)} is ${obj[attr]}`)
+ }
+}
+
+export interface ContestDetail {
+ contest: Contest
+ questions: ContestQuestion[]
+ user_num: number
+ has_chosen_contact: boolean
+ company: Company
+ registered: boolean
+ containsPremium: boolean
+}
+interface Company {
+ name: string
+ description: string
+ logo: string
+ slug?: string
+}
+interface ContestQuestion {
+ id: number
+ question_id: number
+ credit: number
+ title: string
+ english_title: string
+ title_slug: string
+ category_slug: string
+}
+interface Contest {
+ id: number
+ title: string
+ title_slug: string
+ description: string
+ duration: number
+ start_time: number
+ is_virtual: boolean
+ origin_start_time: number
+ is_private: boolean
+ related_contest_title?: unknown
+ discuss_topic_id?: number
+}
+
+export interface CnContestPageData {
+ questionId: string
+ questionIdHash: string
+ questionTitleSlug: string
+ questionTitle: string
+ questionSourceTitle: string
+ questionExampleTestcases: string
+ categoryTitle: string
+ contestTitleSlug: string
+ loginUrl: string
+ isSignedIn: boolean
+ sessionId: string
+ reverseUrl: ReverseUrl
+ enableRunCode: boolean
+ enableSubmit: boolean
+ submitUrl: string
+ interpretUrl: string
+ judgeType: string
+ nextChallengePairs?: unknown
+ codeDefinition: CodeDefinition[]
+ enableTestMode: boolean
+ metaData: MetaData
+ sampleTestCase: string
+ judgerAvailable: boolean
+ envInfo: EnvInfo
+ questionContent: string
+ questionSourceContent: string
+ editorType: string
+}
+interface EnvInfo {
+ cpp: string[]
+ java: string[]
+ python: string[]
+ c: string[]
+ csharp: string[]
+ javascript: string[]
+ ruby: string[]
+ swift: string[]
+ golang: string[]
+ python3: string[]
+ scala: string[]
+ kotlin: string[]
+ rust: string[]
+ php: string[]
+ typescript: string[]
+ racket: string[]
+}
+interface MetaData {
+ name: string
+ params: Param[]
+ return: Return
+}
+interface Return {
+ type: string
+}
+interface Param {
+ name: string
+ type: string
+}
+export interface CodeDefinition {
+ value: string
+ text: string
+ defaultCode: string
+}
+interface ReverseUrl {
+ latest_submission: string
+ account_login: string
+ maintenance: string
+ profile: string
+}
+
+export interface CommonQuestion {
+ translatedContent: string
+ translatedTitle: string
+ questionFrontendId: string
+ metaData: string
+ content: string
+ title: string
+ titleSlug: string
+ codeSnippets: CodeSnippet[]
+ questionId: string
+ questionSourceContent?: string
+}
diff --git a/src/model/memo.ts b/src/model/memo.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9a8d034aecf4317d862c678a918da9212fc9f93a
--- /dev/null
+++ b/src/model/memo.ts
@@ -0,0 +1,17 @@
+import { ResolverParam } from '../provider/resolver'
+
+interface MemoQuestionFile {
+ type: 'question'
+ name: string
+ param: Partial
+}
+interface MemoOtherFile {
+ type: 'other'
+ name: string
+ param: string
+}
+export type MemoFile = MemoQuestionFile | MemoOtherFile
+export interface MemoFolder {
+ name: string
+ children: MemoFile[]
+}
diff --git a/src/model/question.cn.ts b/src/model/question.cn.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5ba0a0a83915a8c36f7fed3eb7b9d01b55cd4775
--- /dev/null
+++ b/src/model/question.cn.ts
@@ -0,0 +1,282 @@
+import { CodeSnippet, FlagType, GraphqlVariables, SubmissionComment } from './common'
+
+export interface ConciseQuestion {
+ fid: string
+ level: number
+ id: number
+ title: string
+ slug: string
+ acs: number
+ submitted: number
+ paid_only: boolean
+ status: string
+ name: string
+}
+export interface MapIdConciseQuestion {
+ [id: number]: ConciseQuestion
+}
+
+export interface TodayRecordData {
+ todayRecord: TodayRecord[]
+}
+export interface TodayRecord {
+ question: Pick
+ lastSubmission: LastSubmission
+ date: string
+ userStatus: UserStatus
+ __typename: string
+}
+interface LastSubmission {
+ id: string
+ __typename: string
+}
+type UserStatus = 'FINISH' | 'NOT_START'
+
+export interface QuestionTranslationData {
+ translations: Translation[]
+}
+export interface Translation {
+ questionId: string
+ title: string
+}
+
+export interface GraphqlResponse {
+ data: T
+}
+export interface DailyQuestionRecordData {
+ dailyQuestionRecords: [DailyQuestionRecord]
+}
+export interface DailyQuestionRecord {
+ date: string
+
+ question: Pick
+ userStatus: string
+ __typename: string
+}
+
+export interface GraphqlRequestData {
+ operationName: string | null
+ query: string
+ variables: GraphqlVariables
+}
+
+export interface QuestionData {
+ question: Question
+}
+export interface Question {
+ questionSourceContent?: string
+ questionId: string
+ questionFrontendId: string
+ boundTopicId: number
+ title: string
+ titleSlug: string
+ content: string
+ translatedTitle: string
+ translatedContent: string
+ isPaidOnly: boolean
+ difficulty: string
+ likes: number
+ dislikes: number
+ isLiked: unknown
+ similarQuestions: string
+ contributors: []
+ langToValidPlayground: string
+ topicTags: TopicTags[]
+ companyTagStats: unknown
+ codeSnippets: CodeSnippet[]
+ stats: string
+ hints: string[]
+ solution: Solution
+ status: Status
+ sampleTestCase: string
+ metaData: string
+ judgerAvailable: boolean
+ judgeType: string
+ mysqlSchemas: string[]
+ enableRunCode: boolean
+ envInfo: string
+ book: unknown
+ isSubscribed: boolean
+ isDailyQuestion: boolean
+ dailyRecordStatus: string
+ editorType: string
+ ugcQuestionId: unknown
+ style: string
+
+ // questionTitleSlug: string
+}
+interface Solution {
+ canSeeDetail: boolean
+ id: string
+}
+type Status = 'ac' | 'notac' | null
+
+interface TopicTags {
+ name: string
+ slug: string
+ translatedName: string
+ questions: number[] //question id
+ __typename: string
+}
+
+export interface ContestData {
+ allContests: Pick[]
+}
+interface Contest {
+ containsPremium: boolean
+ title: string
+ cardImg: string
+ titleSlug: string
+ description: string
+ startTime: number
+ duration: number
+ originStartTime: number
+ isVirtual: boolean
+ company: Company
+ __typename: string
+}
+interface Company {
+ watermark: string
+ __typename: string
+}
+
+export interface TagData {
+ topics: TopicTags[]
+}
+// interface Topics {
+// slug: string
+// name: string
+// questions: number[]
+// translatedName: string
+// }
+
+export interface favoritesData {
+ name: string
+ type: string
+ id: string
+ questions: number[]
+}
+
+export interface SubmitOptions {
+ titleSlug: string
+ typed_code: string
+ question_id: string
+ lang?: string
+}
+export interface SubmitContestOptions extends SubmitOptions {
+ weekname: string
+}
+export interface SubmitResponse {
+ submission_id: number
+}
+export interface CheckResponse {
+ status_code: number
+ lang: string
+ run_success: boolean
+ status_runtime: string
+ memory: number
+ question_id: string
+ elapsed_time: number
+ compare_result: string
+ code_output: string
+ std_output: string
+ last_testcase: string
+ task_finish_time: number
+ task_name: string
+ finished: boolean
+ status_msg: string
+ state: State
+ fast_submit: boolean
+ total_correct: number
+ total_testcases: number
+ submission_id: string
+ runtime_percentile: number
+ status_memory: string
+ memory_percentile: number
+ pretty_lang: string
+}
+
+export interface CheckContestResponse {
+ state: State
+}
+type State = 'PENDING' | 'STARTED' | 'SUCCESS'
+
+export interface CheckOptions {
+ submission_id: number
+ titleSlug: string
+}
+export interface CheckContestOptions extends CheckOptions {
+ weekname: string
+}
+
+export interface SubmissionsResponse {
+ submissionList: SubmissionList
+}
+export interface SubmissionsOptions {
+ lastKey?: string
+ limit?: number
+ offset?: number
+ titleSlug: string
+}
+interface SubmissionList {
+ lastKey: string
+ hasNext: boolean
+ submissions: Submissions[]
+}
+interface Submissions {
+ id: string
+ statusDisplay: string
+ lang: string
+ runtime: string
+ timestamp: string
+ url: string
+ isPending: string
+ memory: string
+ submissionComment?: SubmissionComment
+}
+export interface UpdateCommentOptions {
+ comment: string
+ flagType?: FlagType
+ submissionId: string
+}
+export interface UpdateCommentResponse {
+ submissionCreateOrUpdateSubmissionComment: SubmissionCreateOrUpdateSubmissionComment
+}
+
+interface SubmissionCreateOrUpdateSubmissionComment {
+ ok: boolean
+}
+
+export interface SubmissionDetailOptions {
+ id: string
+}
+export interface SubmissionDetailResponse {
+ submissionDetail?: SubmissionDetail
+}
+interface SubmissionDetail {
+ id: string
+ code: string
+ runtime: string
+ memory: string
+ rawMemory: string
+ statusDisplay: string
+ timestamp: number
+ lang: string
+ passedTestCaseCnt: number
+ totalTestCaseCnt: number
+ sourceUrl: string
+ question: Question
+ outputDetail: OutputDetail
+ __typename: string
+ submissionComment: SubmissionComment
+}
+
+interface OutputDetail {
+ codeOutput: string
+ expectedOutput: string
+ input: string
+ compileError: string
+ runtimeError: string
+ lastTestcase: string
+ __typename: string
+}
diff --git a/src/model/question.ts b/src/model/question.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bab97abd6a11fc2d6c14638e09a61b151836224b
--- /dev/null
+++ b/src/model/question.ts
@@ -0,0 +1,313 @@
+import { CodeSnippet, GraphqlVariables, SubmissionComment } from './common'
+
+export interface Chapter {
+ descriptionText: string
+ id: string
+ slug: string
+ title: string
+}
+export interface ChaptersRes {
+ chapters: Chapter[]
+}
+interface ChapterQuestion {
+ chapterId: number
+ id: string
+ isEligibleForCompletion: boolean
+ paidOnly: boolean
+ prerequisites: unknown[]
+ title: string
+ type: number
+}
+interface ChapterDetail {
+ description: string
+ id: string
+ slug: string
+ title: string
+ items: ChapterQuestion[]
+}
+export interface ChapterRes {
+ chapter: ChapterDetail
+}
+interface GetOrCreateExploreSession {
+ progress: string
+}
+
+interface DailyProgress {
+ is_complete: boolean
+}
+interface DailyQuestionMap {
+ [key: string]: DailyProgress
+}
+export interface DailyWeekMap {
+ [key: string]: DailyQuestionMap
+}
+export interface ChaptersProgressRes {
+ getOrCreateExploreSession: GetOrCreateExploreSession
+}
+interface ChapterItem {
+ question: Pick
+}
+export interface ChapterItemRes {
+ item: ChapterItem
+}
+
+export interface ConciseQuestion {
+ fid: string
+ level: number
+ id: number
+ title: string
+ slug: string
+ acs: number
+ submitted: number
+ paid_only: boolean
+ status: string
+ name: string
+}
+export interface MapIdConciseQuestion {
+ [id: number]: ConciseQuestion
+}
+
+export interface TodayRecordData {
+ todayRecord: TodayRecord[]
+}
+interface TodayRecord {
+ question: Pick
+ lastSubmission: LastSubmission
+ date: string
+ userStatus: UserStatus
+ __typename: string
+}
+interface LastSubmission {
+ id: string
+ __typename: string
+}
+type UserStatus = 'FINISH' | 'NOT_START'
+
+export interface QuestionTranslationData {
+ translations: Translation[]
+}
+interface Translation {
+ questionId: string
+ title: string
+}
+
+export interface GraphqlResponse {
+ data: T
+}
+export interface DailyQuestionRecordData {
+ dailyQuestionRecords: [DailyQuestionRecord]
+}
+export interface DailyQuestionRecord {
+ date: string
+
+ question: Pick
+ userStatus: string
+ __typename: string
+}
+
+export interface GraphqlRequestData {
+ operationName: string | null
+ query: string
+ variables: GraphqlVariables
+}
+
+export interface QuestionData {
+ question: Question
+}
+export interface Question {
+ questionId: string
+ questionFrontendId: string
+ boundTopicId: number
+ title: string
+ titleSlug: string
+ content: string
+ translatedTitle: string
+ translatedContent: string
+ isPaidOnly: boolean
+ difficulty: string
+ likes: number
+ dislikes: number
+ isLiked: unknown
+ similarQuestions: string
+ contributors: []
+ langToValidPlayground: string
+ topicTags: TopicTags[]
+ companyTagStats: unknown
+ codeSnippets: CodeSnippet[]
+ stats: string
+ hints: string[]
+ solution: Solution
+ status: Status
+ sampleTestCase: string
+ metaData: string
+ judgerAvailable: boolean
+ judgeType: string
+ mysqlSchemas: string[]
+ enableRunCode: boolean
+ envInfo: string
+ book: unknown
+ isSubscribed: boolean
+ isDailyQuestion: boolean
+ dailyRecordStatus: string
+ editorType: string
+ ugcQuestionId: unknown
+ style: string
+ __typename: string
+
+ // questionTitleSlug: string
+}
+interface Solution {
+ canSeeDetail: boolean
+ id: string
+}
+type Status = 'ac' | 'notac' | null
+
+interface TopicTags {
+ name: string
+ slug: string
+ translatedName: string
+ questions: number[] //question id
+ __typename: string
+}
+
+export interface ContestData {
+ allContests: Pick[]
+}
+interface Contest {
+ containsPremium: boolean
+ title: string
+ cardImg: string
+ titleSlug: string
+ description: string
+ startTime: number
+ duration: number
+ originStartTime: number
+ isVirtual: boolean
+ company: Company
+ __typename: string
+}
+interface Company {
+ watermark: string
+ __typename: string
+}
+
+export interface TagData {
+ topics: TopicTags[]
+}
+// interface Topics {
+// slug: string
+// name: string
+// questions: number[]
+// translatedName: string
+// }
+
+export interface SubmitOptions {
+ titleSlug: string
+ typed_code: string
+ question_id: string
+ lang?: string
+}
+export interface SubmitContestOptions extends SubmitOptions {
+ weekname: string
+}
+export interface SubmitResponse {
+ submission_id: number
+}
+export interface CheckResponse {
+ status_code: number
+ lang: string
+ run_success: boolean
+ status_runtime: string
+ memory: number
+ question_id: string
+ elapsed_time: number
+ compare_result: string
+ code_output: string
+ std_output: string
+ last_testcase: string
+ task_finish_time: number
+ task_name: string
+ finished: boolean
+ status_msg: string
+ state: State
+ fast_submit: boolean
+ total_correct: number
+ total_testcases: number
+ submission_id: string
+ runtime_percentile: number
+ status_memory: string
+ memory_percentile: number
+ pretty_lang: string
+}
+
+export interface CheckContestResponse {
+ state: State
+}
+type State = 'PENDING' | 'STARTED' | 'SUCCESS'
+
+export interface CheckOptions {
+ submission_id: number
+ titleSlug: string
+}
+export interface CheckContestOptions extends CheckOptions {
+ weekname: string
+}
+
+export interface SubmissionsResponse {
+ submissionList: SubmissionList
+}
+export interface SubmissionsOptions {
+ lastKey?: string
+ limit?: number
+ offset?: number
+ titleSlug: string
+}
+interface SubmissionList {
+ lastKey: string
+ hasNext: boolean
+ submissions: Submissions[]
+}
+export interface Submissions {
+ id: string
+ statusDisplay: string
+ lang: string
+ runtime: string
+ timestamp: string
+ url: string
+ isPending: string
+ memory: string
+ submissionComment?: SubmissionComment
+}
+export interface SubmissionDetailOptions {
+ id: string
+}
+
+export interface SubmissionDetailPageData {
+ submissionData: SubmissionData
+ questionId: string
+ sessionId: string
+ getLangDisplay: string
+ submissionCode: string
+ editCodeUrl: string
+ checkUrl: string
+ runtimeDistributionFormatted: string
+ memoryDistributionFormatted: string
+ langs: unknown[]
+ runtime: string
+ memory: string
+ enableMemoryDistribution: string
+ nonSufficientMsg: string
+}
+interface SubmissionData {
+ status_code: number
+ runtime: string
+ memory: string
+ total_correct: string
+ total_testcases: string
+ compare_result: string
+ input_formatted: string
+ input: string
+ expected_output: string
+ code_output: string
+ last_testcase: string
+}
diff --git a/src/process.ts b/src/process.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d722623a6487be992aa1d0e1eff679ef6c189179
--- /dev/null
+++ b/src/process.ts
@@ -0,0 +1,35 @@
+import * as cp from 'child_process'
+
+
+export const processArr: cp.ChildProcess[] = []
+export function clearProcessArr() {
+ processArr.forEach(p => {
+ p.kill()
+ })
+}
+
+class ChildProcessProxy {
+ processIdSet: Set = new Set()
+ clear() {
+ this.processIdSet.forEach(pid => {
+ process.kill(pid)
+ })
+ }
+ add(p: cp.ChildProcess) {
+ const pid = p.pid
+ this.processIdSet.add(pid)
+ p.once('exit', () => {
+ this.processIdSet.delete(pid)
+ })
+ }
+ remove(p: cp.ChildProcess | null) {
+ if (!p) {
+ return
+ }
+ if (this.processIdSet.has(p.pid)) {
+ p.kill()
+ }
+ }
+}
+
+export const childProcessProxy = new ChildProcessProxy()
\ No newline at end of file
diff --git a/src/provider/codelensProvider.ts b/src/provider/codelensProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33fa1c1a16b32e7cb0c1e444d4128002189bd4d3
--- /dev/null
+++ b/src/provider/codelensProvider.ts
@@ -0,0 +1,146 @@
+import * as vscode from 'vscode'
+import { detectEnableExt, TestCaseParam } from '../common/util'
+import { getFileLang, getFileLangSlug, isAlgorithm, isSupportFile } from '../common/langConfig'
+import { Service } from '../lang/common'
+import {
+ CodeLensesOptions,
+ DebugLensesOptions,
+ GetDescriptionCodeLensesOptions,
+ SubmitCodeLensesOptions,
+ TestCodeLensesOptions,
+ ViewSubmitHistoryCodeLensesOptions,
+} from '../model/command'
+export class CodelensProvider implements vscode.CodeLensProvider {
+ private codeLenses: vscode.CodeLens[] = []
+ private regex: RegExp
+ private _onDidChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter()
+ public readonly onDidChangeCodeLenses: vscode.Event = this._onDidChangeCodeLenses.event
+
+ constructor() {
+ this.regex = /(@test)/g
+
+ vscode.workspace.onDidChangeConfiguration((_) => {
+ this._onDidChangeCodeLenses.fire()
+ })
+ }
+ private getBuildCodeLenses(options: CodeLensesOptions): vscode.CodeLens {
+ const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7))
+
+ codeLens.command = {
+ title: 'build',
+ tooltip: 'build',
+ command: 'algorithm.buildCode',
+ arguments: [options],
+ }
+ return codeLens
+ }
+ private getSubmitCodeLenses(options: SubmitCodeLensesOptions): vscode.CodeLens {
+ const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7))
+
+ codeLens.command = {
+ title: 'submit',
+ tooltip: 'submit',
+ command: 'algorithm.submit',
+ arguments: [options],
+ }
+ return codeLens
+ }
+ private getDescriptionCodeLenses(options: GetDescriptionCodeLensesOptions) {
+ const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7))
+ codeLens.command = {
+ title: 'description',
+ tooltip: 'description',
+ command: 'algorithm.getDescription',
+ arguments: [options],
+ }
+ return codeLens
+ }
+ private getHistoryCodeLenses(options: ViewSubmitHistoryCodeLensesOptions) {
+ const codeLens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 7))
+ codeLens.command = {
+ title: 'history',
+ tooltip: 'history',
+ command: 'algorithm.viewSubmitHistory',
+ arguments: [options],
+ }
+ return codeLens
+ }
+ private getTestCodeLenses(options: TestCodeLensesOptions) {
+ const {
+ testCaseParam: { line },
+ } = options
+ const codeLens = new vscode.CodeLens(new vscode.Range(line, 0, line, 7))
+ codeLens.command = {
+ title: 'test',
+ tooltip: 'test',
+ command: 'algorithm.testCode',
+ arguments: [options],
+ }
+ return codeLens
+ }
+ private getDebugCodeLenses(testCaseParam: TestCaseParam, filePath: DebugLensesOptions) {
+ const { line } = testCaseParam
+ const codeLens = new vscode.CodeLens(new vscode.Range(line, 0, line, 7))
+ codeLens.command = {
+ title: 'debug',
+ tooltip: 'debug',
+ command: 'algorithm.debugCode',
+ arguments: [filePath],
+ }
+ return codeLens
+ }
+
+ public provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] | Thenable {
+ this.codeLenses = []
+ const text = document.getText()
+ const filePath = document.fileName
+ const isSupport = isSupportFile(filePath)
+ if (!isSupport) {
+ return []
+ }
+ const enableExt = detectEnableExt(text, filePath)
+
+ if (enableExt) {
+ const codeLang = getFileLang(filePath)
+ const langSlug = getFileLangSlug(filePath)
+ const codeLensesOptions: CodeLensesOptions = {
+ text,
+ filePath,
+ langSlug,
+ }
+ const submitCodeLenses = this.getSubmitCodeLenses({
+ ...codeLensesOptions,
+ })
+ const buildCodeLenses = this.getBuildCodeLenses({
+ ...codeLensesOptions,
+ })
+ const desCodeLenses = this.getDescriptionCodeLenses({
+ ...codeLensesOptions,
+ })
+ const historyCodeLenses = this.getHistoryCodeLenses({
+ ...codeLensesOptions,
+ })
+ this.codeLenses.push(submitCodeLenses)
+ this.codeLenses.push(buildCodeLenses)
+ this.codeLenses.push(desCodeLenses)
+ this.codeLenses.push(historyCodeLenses)
+ if (isAlgorithm(codeLang)) {
+ const lang = new Service(filePath, text)
+ const testCaseList = lang.getTestCaseList(text)
+ testCaseList.forEach((testCaseParam) => {
+ const testCodeOptions: TestCodeLensesOptions = {
+ filePath: document.uri.fsPath,
+ testCaseParam,
+ }
+ const testCodeLenses = this.getTestCodeLenses(testCodeOptions)
+ const debugCodeLenses = this.getDebugCodeLenses(testCaseParam, document.uri.fsPath)
+ this.codeLenses.push(testCodeLenses)
+ this.codeLenses.push(debugCodeLenses)
+ })
+ }
+
+ return this.codeLenses
+ }
+ return []
+ }
+}
diff --git a/src/provider/completionProvider.ts b/src/provider/completionProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..adef50779757d0ec5f66b97971fb5af201e6b973
--- /dev/null
+++ b/src/provider/completionProvider.ts
@@ -0,0 +1,29 @@
+import { config } from '../config'
+import * as vscode from 'vscode'
+
+const defaultCompletionItems = [
+ {
+ languages: ['javascript', 'typescript'],
+ prefix: 'algorithm',
+ body: "// @algorithm\r\nimport * as a from '" + config.algmModuleDir + "'\r\n\r\n",
+ },
+]
+export function registerCompletionItemProvider(context: vscode.ExtensionContext) {
+ defaultCompletionItems.forEach(({ languages, prefix, body }) => {
+ context.subscriptions.push(
+ vscode.languages.registerCompletionItemProvider(
+ languages,
+ {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
+ const snippetCompletion = new vscode.CompletionItem(prefix)
+ snippetCompletion.insertText = new vscode.SnippetString(body)
+
+ return [snippetCompletion]
+ },
+ },
+ '.' // triggered whenever a '.' is being typed
+ )
+ )
+ })
+}
diff --git a/src/provider/memoProvider.ts b/src/provider/memoProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f088b30668267cd4de1dcfe4553375f4f5e4e60c
--- /dev/null
+++ b/src/provider/memoProvider.ts
@@ -0,0 +1,170 @@
+import * as vscode from 'vscode'
+import { config, updateEnv } from '../config'
+import { sortFiles } from '../util'
+const MemoFilePreviewCommand = 'algorithm.memoFilePreview'
+const MemoFileContext = 'memoFile'
+enum MemoLevel {
+ Folder,
+ File,
+ Invalid,
+}
+enum RemoveMemoMsg {
+ Folder = 'Are you sure you want to delete the folder?',
+ File = 'Are you sure you want to delete the file?',
+}
+
+export class MemoTree extends vscode.TreeItem {
+ constructor(
+ public label: string,
+ public id: string,
+ public paths: string[],
+ public collapsibleState: vscode.TreeItemCollapsibleState = vscode.TreeItemCollapsibleState.Collapsed,
+ public command?: vscode.Command
+ ) {
+ super(label, vscode.TreeItemCollapsibleState.Collapsed)
+ }
+ contextValue = 'memo'
+}
+
+export class MemoProvider implements vscode.TreeDataProvider {
+ private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter<
+ MemoTree | undefined
+ >()
+ readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event
+ refresh(): void {
+ this._onDidChangeTreeData.fire(undefined)
+ }
+ getTreeItem(element: MemoTree): vscode.TreeItem {
+ return element
+ }
+ getChildren(element?: MemoTree): Thenable