开发文档-1
# 开发环境
feigebuge推荐环境
- JDK: JDK 11.0.25
- Gradle: Gradle 8.0
- IDE: IntelliJ IDEA 2023.2.3
- 项目启动的IDE: IntelliJ IDEA 2022.2.3
# HELPER v1.0
# 整体架构

项目分为三层,分别是
UI层:负责和idea交互,编写显示界面
- ToolWindow:工具窗口,插件项目数据显示的主题部位
- Editor:提供特定内容的显示编辑器
- Action:与UI进行交互
Service层:处理业务逻辑,负责数据与UI的交互
Basic层:提供支持环境,目前包括setting和IO模块
setting:项目设置模块,可以在File->Settings部位进行项目设置
IO
file:处理文件IO
console:负责控制台IO
http:负责网络IO,处理与leetcode平台的请求
# Icon图标
一律采用svg作为项目图标, 且svg大小尽量为16x16, 如果偏小, 可选择24x24, 32x32. 最大尽量不要超过32x32
图标命名统一为xxx.svg, xxx_dark.svg
其中, xxx_dark.svg表示IDE如果是dark主题, 则会使用此svg作为图标
关于svg颜色, xxx.svg尽量一律选择fill="", 也就是在IDE主题为light时, 默认的黑色; xxx_dark.svg尽量选择fill="#dbdbdb", 也就是IDE主题为dark时, 使用浅银色
# Setting
该模块用三各类维护
- AppSettings:包含需要idea持久的配置数据,目前需要持久化的数据有三,分别是
langType
,filePath
,coreFilePath
- langType:表示当前选择的语言,当选定langType后,插件会自动创建与langType匹配的代码文件
- filePath:指定代码文件创建目录
- coreFilePath:指定缓存文件创建目录,该值会在用户第一次设置·filePath`时确定,项目只允许创建一次
- AppSettingsComponent:提供设置的UI界面
- AppSettingsConfigurable:配置类,注册到idea当中,提供持久化AppSettings数据的能力
# IO-File
提供与disk,memory交互的能力
架构图

StoreService
目前提供缓存 + 持久化能力
其中缓存能力分为非持久化缓存(cache),持久化缓存(durableCache)。StoreService采用K-V结构存储数据,并在内部通过StoreContent对V进行封装。每一个缓存的数据都会序列化为Json
StoreService提供缓存时间检测功能,但目前该功能并未在项目中真正启用
此外,StoreService
通过@Service
注册为idea Project级别服务,有idea管理类的生命周期。在插件项目关闭之前,会调用StoreService
的persistCahce()方法,将durableCache内的数据写入磁盘,存储为app.properties,磁盘路径由Settings模块的coreFilePath决定,通过AppSetting对外暴露的getCoreFilePath提供存储路径
# IO-HTTP
# 1.总体介绍

使用apache的httpclient框架作为底层请求支持,在此基础上根据lc-runner业务进行封装
总体来说分为两层,第一层HttpClient,封装httpclient提供的基础能力,对外暴露发送get和post请求方法,此外支持cookie的相关操作
第二层是LeetcodeClient,封装HttpClient提供能力,对业务提供各种业务接口,诸如登录判断
,题目获取
,代码提交
等功能,该模块直接对接service
模块
另外,针对http的请求和响应,项目也做了一定封装——HttpRequest
,HttpResponse
# 2.细节描述
leetcode接口采用graphql技术,按需分配资源,关于该技术的详细介绍,可自行搜索
关于该技术对于本项目的影响,体现在Request Body的构建上
在没运用graphql技术前,不同资源是按照url请求路径区分,但应用该技术后,对于国内leetcode来说,大部分资源请求url都是https://leetcode.cn/graphql,而区分资源是依赖请求体
以题目内容查询、用户状态查询接口为例
题目内容查询
url = https://leetcode.cn/graphql
body =
{ "query": "\n query problemsetQuestionList($categorySlug: String, $limit: Int, $skip: Int, $filters: QuestionListFilterInput) {\n problemsetQuestionList(\n categorySlug: $categorySlug\n limit: $limit\n skip: $skip\n filters: $filters\n ) {\n hasMore\n total\n questions {\n acRate\n difficulty\n freqBar\n frontendQuestionId\n isFavor\n paidOnly\n solutionNum\n status\n title\n titleCn\n titleSlug\n topicTags {\n name\n nameTranslated\n id\n slug\n }\n extra {\n hasVideoSolution\n topCompanyTags {\n imgUrl\n slug\n numSubscribed\n }\n }\n }\n }\n}\n ", "variables": { "categorySlug": "all-code-essentials", "skip": 0, "limit": 1, "filters": { "searchKeywords": "3254" } }, "operationName": "problemsetQuestionList" }
1
2
3
4
5
6
7
8
9
10
11
12
用户状态查询
url = https://leetcode.cn/graphql
body =
{ "query": "\n query globalData {\n userStatus {\n isSignedIn\n isPremium\n username\n realName\n avatar\n userSlug\n isAdmin\n useTranslation\n premiumExpiredAt\n isTranslator\n isSuperuser\n isPhoneVerified\n isVerified\n }\n jobsMyCompany {\n nameSlug\n }\n commonNojPermissionTypes\n}\n ", "variables": {} }
1
2
3
4
通过上述两个demo可以发现,运用了graphql技术的接口,请求体的构建相对麻烦
基于此,封装如下三个类,帮助创建请求体

- GraphqlReqBody,负责构建请求体
- SearchParams,是GraphqlReqBody的内部类,负责构建variables变量
- ParamsBuilder,是SearchParams的内部类,通过建造者模式辅助构建SearchParams
# 3. 使用Demo
// 辅助构建varablies
GraphqlReqBody.SearchParams params = new GraphqlReqBody.SearchParams.ParamsBuilder()
.setCategorySlug("all-code-essentials")
.setLimit(limit)
.setSkip(skip)
.build();
// 构建graphql请求体
GraphqlReqBody body = new GraphqlReqBody(LeetcodeApiUtils.PROBLEM_SET_QUERY);
body.setBySearchParams(params);
// 构建httpRequest
HttpRequest httpRequest = new HttpRequest.RequestBuilder(url)
.setBody(body.toJsonStr())
.setContentType("application/json")
.addBasicHeader()
.build();
// send request
HttpResponse httpResponse = httpClient.executePost(httpRequest);
String resp = httpResponse.getBody();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# IO-Console

console模块负责在插件自带的控制台上输出信息
# UI-Action
官方定义:The IntelliJ Platform calls the actions of plugins in response to user interactions with the IDE
Action,通过继承AnAction类来提供用户与IDE的交互能力,在编写完代码后,需要在plugin.xml中进行注册

在本项目中,通过继承AbstractAction
类来规范化每一个Action的行为。在AbstractAction
中,会检查登录状态和设置状态,如果有@LoginPass
或者@SettingPass
注解,那么在对应检测环节则会被放行,否则进行拦截检测
ActionGroup,本质上也是Action,只不过他是一组Action的集合,可以在xml中用<group>标签注册
<!--注册Action-->
<action id="leetcode.plugin.LoginAction" class="com.xhf.leetcode.plugin.actions.loginAction"
text="Sign In" icon="/icons/login.png">
<!--注册Group-->
<group id="leetcode.plugin.lcActionsToolbar">
<reference id="leetcode.plugin.LoginAction"/>
<separator/>
</group>
2
3
4
5
6
7
8
9
# UI-ToolWindow

工具窗口,拓展idea功能的窗口,本项目关于这部分的模块图如下

ToolWindowFactory
负责为ToolWindow
创建内容,Panel
则是ToolWindow
显示内容的载体
ToolWindow由idea自身提供,ToolWIndowFactory负责将Panel添加到ToolWindow内
在本项目中,Panel所在位置如下所示

- LCPanel:包含两部分数据内容,分别是Toolbar工具栏,JBList列表。LCPanel通过继承DataProvider提供对外暴露JBList的能力
- LCConsolePanel:只包含ConsoleView,LCConsolePanel同样通过继承DataProvider提供对外暴露consoleView的能力
# UI-Editor

Editor,提供特定文件的编辑显示能力。比如说普通的文本编辑器,只会显示html代码。但自定义的Html编辑器,则会按照html的语法,显示内容
在idea中,提供自定义文本编辑器依赖于两个类
- FileEditorProvider:负责提供FileEditor,同时检测特定文件是否支持用特定Editor打开
- FileEditor:负责处理文件内容,提供显示的JComponent
在本项目中,为了支持分屏显示代码和题目内容,使用SpliterTextEditorProvider
和SplitTextEditorWithPreview
提供相关能力
- SpliterTextEditorProvider:在提供分屏Editor的同时,检测当前文件是否支持分屏Editor预览。该类通过查询StoreService中是否缓存相关文件信息,来判断当前文件是否支持SplitTextEditor
- SplitTextEditorWithPreview:提供分屏预览能力,在本项目中,第一块分屏是
PsiAwareTextEditorImpl
,第二块分屏是MarkDownEditor
MarkdownEditor,在第一个版本中,并不支持markdown内容的显示,只支持HTML代码的显示。其显示原理是,读取自定义的HTML模板代码,同时将HTML代码注入其中。然后通过JCEF技术,启动
CefBrowser
,通过模拟浏览器的方式打开处理后的HTML内容
# Service-LoginService
loginService,负责处理登录有关的逻辑,利用JCEF技术,模拟启动浏览器,通过加载leetcode登录界面来提供登录功能
需要注意的是,当前leetcode并不支持通过uesrname和password的方式用本地代码模拟登录。leetcode官网存在参数校验的机制,在发出登录请求的同时会携带该参数。该参数只有在官网登录的时候才会携带,单纯的代码模拟无法正确登录
每当JBCefClient
加载状态改变,都会执行监控逻辑,检测当前CefCookieManager
中是否存在name为LEETCODE_SESSION
的Cookie,如果存在,缓存到HTTP模块,同时通过StoreService
进行持久化,登录完成后关闭当前登录窗口,加载题目信息,更新LCPanel
为了判断当前项目是否登录,LoginService
对外暴露isLogin
接口,该接口原理是通过LoginService
内部的loginFlag参数和LeetcodeClient提供的接口共同判断,前者判断插件是否执行成功过登录逻辑,后者判断当前cookie是否能够代表客户端成功登录
# Service-QuestionService
questionService,负责处理question相关逻辑。
loadAllQuestionData
,该方法会在后台查询所有leetcode问题,查询完成后更新LCPanel暴露的JBList
其余业务内容注释中都有解释,没啥好说的
# Service-CodeService

上图表示CodeService与其他模块的沟通信息
首先介绍LeetcodeEditor
,该类用于存储围绕题目内容,创建editor所需要的相关信息
.95971456.png)
- questionId:当前题目的id
- lang:编写当前题目用当的语言类型,比如java或者python
- exampleTestcases:当前题目运行的测试样例【后续可以通过
TestcaseAction
更改】 - markdownContent:题目内容【MarkDownEditor渲染所需的内容来源】
- titleSlug:题目的slug标签,用于唯一标识题目,该字段是http模块发送请求时的必要字段
- defaultTestcases:官方提供的测试用例
LeetcodeEditor会在openCode()
方法中被创建,然后存储到storeService中
openCode()通过点击LCPanel显示的JBList触发,该方法的大致逻辑是
- 填充用户选定的question,丰富其中的信息
- 创建code文件,获取文件路径
- 创建并存储leetcodeEditor,维护额外信息。存贮的key是code文件的文件路径
- 打开文件,并用SplitTextWithPreview显示
runCode(),通过当前打开的FileEditor获取显示的code文件路径,以此作为key,从cache中获取LeetcodeEditor维护的信息,然后执行运行逻辑,将代码通过LeetcodeClient
提交到Leetcode平台。运行的结果在通过ConsoleUtils
输出到控制台
TestCase(),与runcode类似,通过code 路径获取leetcodeEditor信息。创建Dialog与用户交互,将用户提供的测试数据存入LeetcodeEditor内,更新测试用例
Tip:在runCode()方法中,通过
AbstractResultBuilder
将运行结果转化为String,然后再将String交由ConsoleUtils输出有关于
AbstractResultBuilder
类的详细内容,请参考源码
# v2.0
新增内容
- 登录模块,新增cookie登录
- editor模块,支持同时阅览
题目内容
,题解
,提交记录
。支持html于markdown的内容阅览 - 新增http处理器
- 新增事件总线,解耦模块
# LoginService-CookieLoginWindow

Cookie登录和Jcef登录存在通用逻辑,因此向上抽取抽象类
BasicWindow通过start()方法启动,getFrameTItle(), loadConponent()方法下放到子类实现。前者要求子类设置启动window的title,后者要求子类填充window的内容
此外,CookieLoginWindow用JTextArea要求用户输入名为LEETCODE_SESSTION
的Cookie信息
# UI-Editor

Editor模块稍显复杂,文档将会按照等级依次介绍
# 1. SplitTextEditorProvider 和SplitTextEditorWithPreview
SplitTextEditorProvider 提供SplitTextEditorWithPreview,SplitTextEditorWithPreview提供第一层的分屏预览能力,左侧是编码区域,右侧是内容预览区域

# 2.PsiAwareTextEditorImpl和FocusTextEditor
PsiAwareTextEditorImpl作为SplitTextEditorWithPreview左侧的编辑区域,提供代码编辑能力
FocusTextEditor作为右侧编辑区域,提供tabs显示能力,包含三个tab,每个tab的显示内容由3个FileEditor维护

# 3.MarkDownEditor、SolutionEditor、SubmissionEditor
# i.MarkDownEditor
MarkDownEditor,提供HTML、Markdown内容的显示能力,其原理是通过JCEF加载处理后的HTML文件,通过模拟浏览器选软HTML内容
Markdown内容渲染,使用前端vditor框架渲染。因为该框架在项目首次启动渲染Markdown耗时很长,因此在渲染HTML的时候会提前加载需要的js、css文件

# ii.SolutionEditor、SubmissionEditor
SolutionEditor、SubmissionEditor分别提供题解、提交记录显示


上述两种FileEditor都需要分屏显示功能,因此向上抽取AbstractSplitTextEditor
抽象类,提供分窗能力
AbstractSplitTextEditor
.52553f96.png)
AbstractSplitTextEditor核心组件
jbSplitter
,提供分屏显示的能力createToolbarWrapper()为component创建工具栏,提供关闭右侧分屏的能力
initFirstComp()的实现下放到子类,初始化分屏左侧内容
openSecond(String)的实现下放到子类,创建右侧分屏内容

# IO-HTTP
当前http模块通过web方式,提供resource目录下的资源内容。当前模块服务于markdown内容显示
本项目中,采用JCEF方式模拟浏览器,使用前端框架vditor
渲染markdown内容。markdown的内容显示依赖于框架js、css代码,因此需要提供web能力,加载文件内容
LocalHttpRequestHandler
负责拦截idea发出的http请求,如果请求前缀是"/leetcode-project/",则交由LocalResourceController
负责处理,读取resource目录下的js、css文件内容
# Bus

总线模块,提供注册和发送消息的能力。订阅方注册到特定的Topic内,接受投放到Topic中的消息内容
LCSubscriber
,订阅者,当特定Topic内存在消息,订阅者的onEvent()
方法将会被触发
LCEventBus
,提供消息投递和订阅者注册功能。LCEventBus
允许将订阅者注册到内部的订阅池,并按照不同的Topic进行存储,每个Topic是LCEvent
的子类的class。当有观察者通过LCEventBus
投递消息,LCEventBus
将会通知订阅相关Topic的所有订阅者