idea插件開發--服務-翻譯插件

a18792721831 2022-01-08 03:31:33 阅读数:410

idea 插件 插件

gitee地址:https://gitee.com/jyq_18792721831/studyplugin.git
idea插件開發入門
idea插件開發–配置
idea插件開發–服務-翻譯插件

介紹

本次主要介紹idea中服務的相關內容,包括服務的種類,服務的定義,服務的獲取,以及服務的使用。

之後綜合idea插件的Action和簡單配置,實現一個較為實用的翻譯小插件,以此複習和鞏固idea插件的Action和簡單配置。

服務

在spring中,服務一般是單例的,使用起來也比較方便,自動注入。

idea插件平臺也提供了類似的解决方案,允許我們創建單例的服務,然後在使用的時候獲取。

在idea插件項目中,通過com.intellij.openapi.components.ComponentManager接口獲取,服務會在第一次調用的時候創建一個實例,而且在作用域範圍內,保證只有一個實例。服務應該實現Disposable接口,用於注銷服務。

ComponentManager接口有這幾個實現類

image-20220102175856461

最長用的也就是Application了

idea提供三種類型的服務:application,prject和module級別的服務。其作用域分別是全局,項目和模塊。模塊級別的服務需要慎用,因為當項目中模塊比較多的時候,會占用較多的內存和資源。

對於project和module級別的服務,可以注入project和module的對象,注入方式為在構造函數中增加Project和Module參數。因為這個注入的構造函數主要是用於參數注入,所以在使用的時候,盡可能避免在自己的代碼中調用。

輕量級服務

在2019.3版本之後,增加了另一種輕量級的服務,輕量級服務不需要在plugin.xml中定義,只需要增加@Service注解即可。

  • 輕量級服務必須是final修飾
  • 輕量級服務不推薦使用構造函數注入(根據文檔給出的示例,project對象還是能够使用構造函數注入)
  • 如果服務用於存儲(PersistentStateComponent,那麼需要增加參數roamingType=RoamingtType.DISABLED)

服務定義

如果不是輕量級服務,那麼需要在plugin.xml中定義,定義需要在extensions節點下定義。定義不同作用域的服務,使用不同的標簽:applicationService,projectService,moduleService

定義服務,接口不是必須的,如果沒有接口,把接口和實例屬性設置成實現類就行。

服務獲取

可以使用ComponentManager接口的實例獲取接口,常使用ApplicationManager獲取。

實例

目標

實現一個翻譯插件,說實話,本人英語水平是在有限,所以在開發編碼的時候,有時候給變量起名字,就需要翻譯好,在拷貝過來。

當然,現在在插件市場上也有許許多多的翻譯插件,做的功能齊全,使用方便。

我們這主要是學習插件開發,翻譯插件邏輯也不複雜,正好作為一個練手的項目。

需求:在編輯窗口,選中需要翻譯的中文,按下快捷鍵,翻譯為英文,並轉為駝峰形式,替換選中的中文。

分解

  1. 我們需要增加編輯窗口的Action,而且需要有快捷鍵
  2. 需要獲取選中的中文
  3. 需要翻譯接口
  4. 配置在線翻譯接口的參數
  5. 得到翻譯的英文,處理為駝峰形式
  6. 替換編輯窗口選中的中文

准備在線翻譯信息

有道翻譯

有道智雲AI開放平臺 (youdao.com)注册賬號,注册送50人民幣,自己玩足够了。

然後創建文本翻譯的應用

image-20220102160534069

在個人信息裏能看到應用id和秘鑰

image-20220102160748148

必應翻譯

Bing for Partners helps businesses and developers succeed注册賬號,必應在線文本翻譯每月有免費的數量,個人使用完全足够

image-20220102160936187

有了賬號後,根據快速入門:Translator 入門 - Azure Cognitive Services | Microsoft Docs選擇文本翻譯即可

image-20220102161259057

注册需要visa卡等進行驗證,如果沒有就跳過(我就沒有)

image-20220102161558966

百度翻譯

百度翻譯開放平臺 (baidu.com)注册賬號,選擇通用翻譯

image-20220102161714933

這裏需要進行實名認證,並注册為個人開發者,然後在控制臺就能看到自己選擇的服務了

image-20220102162646203

在最下面有應用id和秘鑰

image-20220102162725864

創建插件項目

創建如下項目

image-20220102153309722

plugin.xml中定義好插件的信息

image-20220102153448932

創建配置界面

在ui包下創建配置的信息

image-20220102153746510

然後通過拖動的方式增加控件

image-20220102163324361

需要注意,使用密碼輸入框,而不是文本輸入框

記得選擇生成源代碼

image-20220102163351964

在源代碼中,我們增加方法,用於獲取數據,這樣就不把控件進行暴露了

編譯才會生成源代碼

然後生成測試ui的main方法(需要給最外層的JPanel設置屬性名字)

image-20220102164018761

運行main方法就可以看看我們的界面效果了

image-20220102164143356

需要注意,我們需要將最外層的JPannel暴露到外面,雖然自己生成了一個暴露最外層JPannel的方法,但是不介意使用。

如果用戶是修改配置,我們還需要增加方法,用於設置控件的值

image-20220102211208095

引入第三方依賴

我們使用lombok注解進行暴露,要是用lombok就需要在項目中加入lombok的依賴。

還記得我們的項目結構中,有個lib的文件夾。

lib文件夾就是放第三方依賴的jar包的。

首選需要在Maven Central Repository Search搜索lombok插件,然後下載jar包,並將jar拷貝到lib目錄下。

image-20220102172122399

然後將增加的jar包加入項目

image-20220102172201293

當然,其他第三方jar包也是這樣增加的。

定義存儲的服務

我們使用之前說的最簡單的存儲方式,然後對這種方式進行封裝。服務使用輕量級的服務,直接使用注解,也不需要實現注銷的方法。

import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.components.Service;
@Service
public final class TranslateAppInfoService {

private final PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();
public void save(String key, String value) {

propertiesComponent.setValue(key, value);
}
public String get(String key, String defaultValue) {

return propertiesComponent.getValue(key, defaultValue);
}
public String get(String key) {

return get(key, "");
}
}

我們封裝三個方法,一個是存儲,一個是獲取,一個是帶有默認值的獲取。

很簡單,這裏定義的存儲服務,會在SearchableConfiguable的實現類中使用。

定義配置界面

我們創建好了配置界面的UI後,還需要配置到setting下,idea插件開發–配置_a18792721831的博客-CSDN博客

首先創建SearchableConfigurable接口的實現類,傳輸配置id,配置名字。

在定義配置界面的時候,首先從存儲服務中獲取已經保存的配置,然後把配置放入控件中,因為用戶可能只想修改一部分,如果不設置,就會被空值覆蓋,而且不設置,用戶也不知道哪些已經配置過了。所以需要在創建好控件from後,獲取已有配置,設置到控件。

如果用戶根本無修改,此時給isModified方法返回false,錶示應用按鈕不可用,無修改,無需保存,無需調用apply方法。

在apply方法中,則是將控件中輸入的值,調用存儲服務,存儲起來。

完整代碼如下

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.text.StringUtil;
import com.study.plugin.translate.service.TranslateAppInfoService;
import com.study.plugin.translate.ui.TranslateConfigUI;
import com.study.plugin.translate.utils.PluginAppKeys;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.JComponent;
public class TranslateAppInfoConfig implements SearchableConfigurable, PluginAppKeys {

private TranslateConfigUI ui = new TranslateConfigUI();
private TranslateAppInfoService appInfoService = ApplicationManager.getApplication().getService(TranslateAppInfoService.class);
@Override
public @NotNull
@NonNls
String getId() {

return PLUGIN_CONFIG_ID;
}
@Override
public @NlsContexts.ConfigurableName String getDisplayName() {

return PLUGIN_CONFIG_NAME;
}
@Override
public @Nullable
JComponent createComponent() {

ui.setYoudaoAppId(appInfoService.get(YOUDAO_APP_ID_SAVE_KEY, ""));
return ui.getRootJPanel();
}
@Override
public boolean isModified() {

if (!appInfoService.get(YOUDAO_APP_ID_SAVE_KEY).equals(ui.getYoudaoAppId()) ||
!appInfoService.get(YOUDAO_APP_SECRET_SAVE_KEY).equals(ui.getYoudaoAppSecret()) ||
!appInfoService.get(BIYING_APP_ID_SAVE_KEY).equals(ui.getBiyingAppId()) ||
!appInfoService.get(BIYING_APP_SECRET_SAVE_KEY).equals(ui.getBiyingAppSecret()) ||
!appInfoService.get(BAIDU_APP_ID_SAVE_KEY).equals(ui.getBaiduAppId()) ||
!appInfoService.get(BAIDU_APP_SECRET_SAVE_KEY).equals(ui.getBaiduAppSecret())) {

return true;
}
return false;
}
@Override
public void apply() throws ConfigurationException {

String youdaoAppId = ui.getYoudaoAppId();
String youdaoAppSecret = ui.getYoudaoAppSecret();
if (StringUtil.isNotEmpty(youdaoAppId) && StringUtil.isNotEmpty(youdaoAppSecret)) {

appInfoService.save(YOUDAO_APP_ID_SAVE_KEY, youdaoAppId);
appInfoService.save(YOUDAO_APP_SECRET_SAVE_KEY, youdaoAppSecret);
}
String biyingAppId = ui.getBiyingAppId();
String biyingAppSecret = ui.getBiyingAppSecret();
if (StringUtil.isNotEmpty(biyingAppId) && StringUtil.isNotEmpty(biyingAppSecret)) {

appInfoService.save(BIYING_APP_ID_SAVE_KEY, biyingAppId);
appInfoService.save(BIYING_APP_SECRET_SAVE_KEY, biyingAppSecret);
}
String baiduAppId = ui.getBaiduAppId();
String baiduAppSecret = ui.getBaiduAppSecret();
if (StringUtil.isNotEmpty(baiduAppId) && StringUtil.isNotEmpty(baiduAppSecret)) {

appInfoService.save(BAIDU_APP_ID_SAVE_KEY, baiduAppId);
appInfoService.save(BAIDU_APP_SECRET_SAVE_KEY, baiduAppSecret);
}
}
}

最後別忘記在plugin.xml中注册

 <extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<notificationGroup displayType="BALLOON" id="simpleconfig.notification.balloon" isLogByDefault="false"/>
<applicationConfigurable parentId="tools" instance="com.study.plugin.translate.config.TranslateAppInfoConfig" id="com.study.plugin.translate。setting.config.id" displayName="在線翻譯信息"/>
</extensions>

當做好這些後,就可以調試一下之前寫的代碼了

image-20220102211337847

還是很不錯的,簡單明了,記得測試下存儲服務是否正常。

創建Action

創建Action很簡單,之前就用過:idea插件開發入門_a18792721831的博客-CSDN博客

我們創建Action,快捷鍵還是使用ctrl+alt+;

image-20220103172724431

在觸發後,首先獲取選中的文本,然後調用翻譯的服務(假設我們已經寫好了一個翻譯的RestAPI)

image-20220103172853719

以此來觸發翻譯,等翻譯至少有一個可用時,在回頭基礎開發這裏的操作。

封裝抽象RestAPI

因為我們使用的都是在線API的方式請求的,所以需要使用URL請求。

為了使用更加方便,我們使用spring的restTemplate接口進行請求。

首先從maven倉庫下載spring-beans,spring-context,spring-web,spring-core四個依賴,並加入項目。

image-20220103120435414

然後封裝Rest請求的抽象類。

抽象類中主要是restTemplate的對象和存儲服務的對象,因為對所有的各個廠商的在線翻譯平臺來說,我們的restTemplate和存儲服務使用同一個就可以了,而且我們定義子類必須實現翻譯方法,翻譯方法傳入待翻譯的中文,返回翻譯後的英文或者空串。

一些公共的工具方法,也可以放在抽象類中,比如加密

import com.intellij.openapi.application.ApplicationManager;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
public abstract class TranslateRestService {

protected RestTemplate restTemplate;
protected volatile AtomicBoolean isInit = new AtomicBoolean(Boolean.FALSE);
protected TranslateAppInfoService appInfoService = ApplicationManager.getApplication().getService(TranslateAppInfoService.class);
protected synchronized void init() {

// 如果已經初始化了,直接結束
if (isInit.get()) {

return;
}
// 連接池
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
poolingHttpClientConnectionManager.setMaxTotal(4);
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(2);
// 我們目前只有2個在線翻譯可用,每個翻譯2個線程用於Rest請求,所以設置最大連接4,每個翻譯api是2個並發
// 客戶端構造器
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
// 創建restTemplate
HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
httpRequestFactory.setHttpClient(httpClientBuilder.build());
httpRequestFactory.setConnectTimeout(6000);
httpRequestFactory.setConnectTimeout(6000);
httpRequestFactory.setReadTimeout(12000);
RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
this.restTemplate = restTemplate;
isInit.compareAndSet(Boolean.FALSE, Boolean.TRUE);
}
/** * 加密 * * @param string * @return */
protected static String getDigest(String string, String key) {

if (string == null) {

return null;
}
char hexDigits[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
byte[] btInput = string.getBytes(StandardCharsets.UTF_8);
try {

MessageDigest mdInst = MessageDigest.getInstance(key);
mdInst.update(btInput);
byte[] md = mdInst.digest();
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (byte byte0 : md) {

str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (NoSuchAlgorithmException e) {

return null;
}
}
public abstract String translate(String word);
}

當抽象方法完成後,就需要針對各個廠商實現翻譯的子類。

有道翻譯

根據有道翻譯的api產品文檔-自然語言翻譯服務 (youdao.com),根據裏面的示例程序,拷貝相關的請求參數封裝的代碼到子類中,然後調用父類的存儲服務,進行請求app_id,app_secret的讀取,並使用父類的restTemplate進行請求,並返回。

import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.YoudaoTranslateResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.util.HashMap;
import java.util.Map;
@Service
public final class YoudaoTranslateRestService extends TranslateRestService implements PluginAppKeys {

private String HOST = "https://openapi.youdao.com/api";
private String APP_ID = appInfoService.get(YOUDAO_APP_ID_SAVE_KEY);
private String APP_SECRET = appInfoService.get(YOUDAO_APP_SECRET_SAVE_KEY);
private String DIGEST_KEY = "SHA-256";
public YoudaoTranslateRestService() {

super();
if (!isInit.get()) {

super.init();
}
}
@Override
public String translate(String word) {

Map<String, String> params = getParams(word);
StringBuilder builder = new StringBuilder(HOST + "?");
params.entrySet().forEach(ent -> {

builder.append(ent.getKey() + "=" + ent.getValue() + "&");
});
String requestUrl = builder.toString();
requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
YoudaoTranslateResult result = restTemplate.getForObject(requestUrl, YoudaoTranslateResult.class);
if (result.getErrorCode().equals("0")) {

return result.getTranslation().get(0);
}
return null;
}
private Map<String, String> getParams(String word) {

Map<String, String> params = new HashMap<>();
String salt = String.valueOf(System.currentTimeMillis());
params.put("from", "auto");
params.put("to", "en");
params.put("signType", "v3");
String curtime = String.valueOf(System.currentTimeMillis() / 1000);
params.put("curtime", curtime);
String signStr = APP_ID + truncate(word) + salt + curtime + APP_SECRET;
String sign = getDigest(signStr, DIGEST_KEY);
params.put("appKey", APP_ID);
params.put("q", word);
params.put("salt", salt);
params.put("sign", sign);
return params;
}
public static String truncate(String q) {

if (q == null) {

return null;
}
int len = q.length();
return len <= 20 ? q : (q.substring(0, 10) + len + q.substring(len - 10, len));
}
}

不要忘記把子類定義為輕量級的服務。這裏我們還沒有做英文單詞的駝峰化。

這裏需要將返回值封裝為對象,根據文檔中給出的返回信息,我們只需要處理一定返回的項目即可。

所以,增加有道返回的對象:

image-20220103164300081

調用這些接口,可能出現各種問題,需要找廠商的客服進行調試。

百度翻譯

百度翻譯也差不多,根據百度翻譯開放平臺 (baidu.com)文檔,找到示例程序,拷貝到子類,進行調用。

import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.BaiduTranslateResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.util.CollectionUtils;
@Service
public final class BaiduTranslateRestService extends TranslateRestService implements PluginAppKeys {

private String HOST = "http://api.fanyi.baidu.com/api/trans/vip/translate";
private String APP_ID = appInfoService.get(BAIDU_APP_ID_SAVE_KEY);
private String APP_SECRET = appInfoService.get(BAIDU_APP_SECRET_SAVE_KEY);
private String DIGEST_KEY = "MD5";
public BaiduTranslateRestService() {

super();
if (!isInit.get()) {

super.init();
}
}
@Override
public String translate(String word) {

Map<String, String> params = getParams(word);
StringBuilder builder = new StringBuilder(HOST + "?");
params.entrySet().forEach(ent -> {

builder.append(ent.getKey() + "=" + ent.getValue() + "&");
});
String requestUrl = builder.toString();
requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
BaiduTranslateResult result = restTemplate.getForObject(requestUrl, BaiduTranslateResult.class);
if (Objects.isNull(result.getError_code()) && !CollectionUtils.isEmpty(result.getTrans_result())) {

return result.getTrans_result().get(0).getDst();
}
return null;
}
private Map<String, String> getParams(String word) {

Map<String, String> params = new HashMap<String, String>();
params.put("q", word);
params.put("from", "zh");
params.put("to", "en");
params.put("appid", APP_ID);
// 隨機數
String salt = String.valueOf(System.currentTimeMillis());
params.put("salt", salt);
// 簽名
String src = APP_ID + word + salt + APP_SECRET; // 加密前的原文
params.put("sign", getDigest(src, DIGEST_KEY).toLowerCase());
return params;
}
}

百度翻譯的返回對象定義

image-20220103170347114

image-20220103170357197

廠商擴展

上面兩個廠商的免費額度有限,或者說因為各種原因,無法使用,那麼可以選擇另外其他的廠商。

所以廠商的擴展就很有必要。

以DeepL翻譯為例,這是一個提供機器翻譯的網站,根據介紹是使用機器學習,實現的在線翻譯。

當然也需要注册賬號信息,獲取app_id和app_secret。DeepL翻譯API|機器翻譯技術

其技術文檔在這裏:DeepL API

每增加一個廠商,就需要同步增加配置信息。

所以我們根據廠商要求,增加相應的配置界面。

image-20220103123742228

然後在界面中增加數據設置和讀取的方法

image-20220103124305719

接著和其他配置相同的處理,在初始化界面時,將已有的值放入,判斷是否修改,然後進行保存

image-20220103124358769

然後實現抽象的RestAPI類,定義deepl的子類翻譯

import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.DeeplResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.groovy.util.Maps;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@Service
public final class DeeplTranslateRestService extends TranslateRestService implements PluginAppKeys {

private String HOST = "https://api-free.deepl.com/v2/translate";
private String APP_SECRET = appInfoService.get(DEEPL_APP_SECRET_SAVE_KEY);
public DeeplTranslateRestService() {

super();
if (!isInit.get()) {

init();
}
}
@Override
public String translate(String word) {

Map<String, String> params = getParams(word);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("Content-Type", "application/x-www-form-urlencoded");
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
params.entrySet().forEach(ent -> {

map.put(ent.getKey(), Collections.singletonList(ent.getValue()));
});
RequestEntity<MultiValueMap<String, String>> request = new RequestEntity<>(map, httpHeaders, HttpMethod.POST, URI.create(HOST));
DeeplResult result = restTemplate.postForObject(HOST, request, DeeplResult.class, Maps.of("auth_key", APP_SECRET));
if (Objects.nonNull(result) && !CollectionUtils.isEmpty(result.getTranslations())) {

return result.getTranslations().get(0).getText();
}
return null;
}
private Map<String, String> getParams(String word) {

Map<String, String> params = new HashMap<>();
params.put("text", word);
// 非必填
params.put("source_lang", "ZH");
params.put("target_lang", "EN-US");
params.put("auth_key", APP_SECRET);
return params;
}
}

需要注意的是我們使用的Service注解是idea-platfrom的,而不是spring的。

然後在Action中調用即可。

image-20220103125423846

暫時我們只是將翻譯的結果使用通知輸出,實際在Action中還應該對英文單詞結果做駝峰化,以及多個廠商之間的調度操作,還有就是需要替換選中的中文。現在還剩下這些未完成,當然這些前提是你至少有一個廠商能進行翻譯。

編寫Action後續操作

首先我們有多個用於翻譯的RestApi,所以我們創建一個調度工具,調度工具也非常簡單,就是輪訓。

image-20220103174129318

當我們得到了翻譯後的英文語句後,需要轉為駝峰形式

因為我們翻譯可能翻譯的是一個詞組,當翻譯的是詞組的時候,返回的就不是單詞,而是短語,短語是通過空格分割的,所以我們需要將返回的英文字符串根據空格拆分,然後第一個單詞轉為小寫,取餘單詞的第一個首字母大寫,然後拼接起來就行了

image-20220103194740231

接著我們需要控制什麼時候可用翻譯功能,當用戶沒有選中字符的時候,是不能使用字符的

image-20220103194836132

最後一步,我們需要使用翻譯後的英文字符串,並且是轉為駝峰形式的字符串替換掉選中的中文字符

image-20220103195008269

效果

image-20220103195038091

image-20220103195059886

打包

打包直接使用ide的打包功能即可

image-20220103195712084

打包後的zip包就可以發布給其他人使用了

image-20220103195815886

最後的最後

配置的地方增加點說明,告訴用戶該去哪裏注册。

image-20220103202630840

增加的文本區不可編輯。

總結

通過這個小插件,學習了idea插件中服務的定義,服務的獲取和使用。

通過調用在線翻譯API,學習了restTemplate的使用和配置。

通過各個廠商的擴展,進一步理解了抽象,以及抽象類和子類之間的關系,換句話說,這一定程度上增加了我的抽象能力。

而Action的各種邏輯,則是對idea插件平臺有了進一步的理解,包括如何替換選中的字符,如何控制插件功能是否可用等等。

版权声明:本文为[a18792721831]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201080331325626.html