feat: update idea
This commit is contained in:
parent
c6e744e593
commit
56d3fda1d7
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
3
babel.config.js
Normal file
3
babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||
};
|
5
globals.d.ts
vendored
Normal file
5
globals.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
interface Window {
|
||||
JSJavaBridge: any;
|
||||
acquireVsCodeApi: any;
|
||||
IdeaToJSMessage: any;
|
||||
}
|
@ -54,7 +54,7 @@
|
||||
"test": "mocha",
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"dev": "webpack serve --config webpack.config.js --open",
|
||||
"idea": "webpack --config webpack.idea.config.js && mv dist/main.js dist/main.html ../devchat-intellij/src/main/resources/static && echo '🎆done'"
|
||||
"idea": "webpack --config webpack.idea.config.js && mv dist/main.js dist/main.html ../src/main/resources/static && echo '🎆done'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
|
@ -1,246 +0,0 @@
|
||||
// src/apiKey.ts
|
||||
|
||||
import { UiUtilWrapper } from './uiUtil';
|
||||
|
||||
export class ApiKeyManager {
|
||||
static toProviderKey(provider: string) : string | undefined {
|
||||
let providerNameMap = {
|
||||
"openai": "OpenAI",
|
||||
"devchat": "DevChat"
|
||||
};
|
||||
return providerNameMap[provider];
|
||||
}
|
||||
static async getApiKey(llmType: string = "OpenAI"): Promise<string | undefined> {
|
||||
const llmModelT = await this.llmModel();
|
||||
if (!llmModelT) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return llmModelT.api_key;
|
||||
}
|
||||
|
||||
static async getValidModels(): Promise<string[]> {
|
||||
const modelProperties = async (modelPropertyName: string, modelName: string) => {
|
||||
const modelConfig = UiUtilWrapper.getConfiguration("devchat", modelPropertyName);
|
||||
if (!modelConfig) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let modelProperties: any = {};
|
||||
for (const key of Object.keys(modelConfig || {})) {
|
||||
const property = modelConfig![key];
|
||||
modelProperties[key] = property;
|
||||
}
|
||||
if (!modelConfig["provider"]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const apiKey = await this.getProviderApiKey(modelConfig["provider"]);
|
||||
if (apiKey) {
|
||||
modelProperties["api_key"] = apiKey;
|
||||
} else {
|
||||
const apiKeyDevChat = await this.getProviderApiKey("devchat");
|
||||
if (apiKeyDevChat) {
|
||||
modelProperties["api_key"] = apiKeyDevChat;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
modelProperties['model'] = modelName;
|
||||
return modelProperties;
|
||||
};
|
||||
|
||||
let modelList : string[] = [];
|
||||
const openaiModel = await modelProperties('Model.gpt-3-5', "gpt-3.5-turbo");
|
||||
if (openaiModel) {
|
||||
modelList.push(openaiModel.model);
|
||||
}
|
||||
const openaiModel1 = await modelProperties('Model.gpt-3-5-1106', "gpt-3.5-turbo-1106");
|
||||
if (openaiModel1) {
|
||||
modelList.push(openaiModel1.model);
|
||||
}
|
||||
const openaiModel2 = await modelProperties('Model.gpt-3-5-16k', "gpt-3.5-turbo-16k");
|
||||
if (openaiModel2) {
|
||||
modelList.push(openaiModel2.model);
|
||||
}
|
||||
const openaiModel3 = await modelProperties('Model.gpt-4', "gpt-4");
|
||||
if (openaiModel3) {
|
||||
modelList.push(openaiModel3.model);
|
||||
}
|
||||
const openaiModel4 = await modelProperties('Model.gpt-4-turbo', "gpt-4-1106-preview");
|
||||
if (openaiModel4) {
|
||||
modelList.push(openaiModel4.model);
|
||||
}
|
||||
const claudeModel = await modelProperties('Model.claude-2', "claude-2");
|
||||
if (claudeModel) {
|
||||
modelList.push(claudeModel.model);
|
||||
}
|
||||
const xinghuoModel = await modelProperties('Model.xinghuo-2', "xinghuo-2");
|
||||
if (xinghuoModel) {
|
||||
modelList.push(xinghuoModel.model);
|
||||
}
|
||||
const glmModel = await modelProperties('Model.chatglm_pro', "chatglm_pro");
|
||||
if (glmModel) {
|
||||
modelList.push(glmModel.model);
|
||||
}
|
||||
const erniebotModel = await modelProperties('Model.ERNIE-Bot', "ERNIE-Bot");
|
||||
if (erniebotModel) {
|
||||
modelList.push(erniebotModel.model);
|
||||
}
|
||||
const llamaCode2Model = await modelProperties('Model.CodeLlama-34b-Instruct', "CodeLlama-34b-Instruct");
|
||||
if (llamaCode2Model) {
|
||||
modelList.push(llamaCode2Model.model);
|
||||
}
|
||||
const llama70BModel = await modelProperties('Model.llama-2-70b-chat', "llama-2-70b-chat");
|
||||
if (llama70BModel) {
|
||||
modelList.push(llama70BModel.model);
|
||||
}
|
||||
|
||||
return modelList;
|
||||
}
|
||||
|
||||
static async llmModel() {
|
||||
let llmModelT = UiUtilWrapper.getConfiguration('devchat', 'defaultModel');
|
||||
if (!llmModelT) {
|
||||
const validModels = await this.getValidModels();
|
||||
if (validModels.length > 0) {
|
||||
await UiUtilWrapper.updateConfiguration('devchat', 'defaultModel', validModels[0]);
|
||||
llmModelT = validModels[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const modelProperties = async (modelPropertyName: string, modelName: string) => {
|
||||
const modelConfig = UiUtilWrapper.getConfiguration("devchat", modelPropertyName);
|
||||
if (!modelConfig) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let modelProperties: any = {};
|
||||
for (const key of Object.keys(modelConfig || {})) {
|
||||
const property = modelConfig![key];
|
||||
modelProperties[key] = property;
|
||||
}
|
||||
if (!modelConfig["provider"]) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const apiKey = await this.getProviderApiKey(modelConfig["provider"]);
|
||||
const apiBase = await this.getProviderApiBase(modelConfig["provider"]);
|
||||
|
||||
if (apiKey) {
|
||||
modelProperties["api_key"] = apiKey;
|
||||
} else {
|
||||
const apiKeyDevChat = await this.getProviderApiKey("devchat");
|
||||
if (apiKeyDevChat) {
|
||||
modelProperties["api_key"] = apiKeyDevChat;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (apiBase) {
|
||||
modelProperties["api_base"] = apiBase;
|
||||
} else if (!apiKey) {
|
||||
const devchatApiBase = await this.getProviderApiBase("devchat");
|
||||
if (devchatApiBase) {
|
||||
modelProperties["api_base"] = devchatApiBase;
|
||||
}
|
||||
}
|
||||
|
||||
if (!modelProperties["api_base"] && modelProperties["api_key"]?.startsWith("DC.")) {
|
||||
modelProperties["api_base"] = "https://api.devchat.ai/v1";
|
||||
}
|
||||
|
||||
modelProperties['model'] = modelName;
|
||||
return modelProperties;
|
||||
};
|
||||
|
||||
if (llmModelT === "gpt-3.5-turbo") {
|
||||
return await modelProperties('Model.gpt-3-5', "gpt-3.5-turbo");
|
||||
}
|
||||
if (llmModelT === "gpt-3.5-turbo-1106") {
|
||||
return await modelProperties('Model.gpt-3-5-1106', "gpt-3.5-turbo-1106");
|
||||
}
|
||||
if (llmModelT === "gpt-3.5-turbo-16k") {
|
||||
return await modelProperties('Model.gpt-3-5-16k', "gpt-3.5-turbo-16k");
|
||||
}
|
||||
if (llmModelT === "gpt-4") {
|
||||
return await modelProperties('Model.gpt-4', "gpt-4");
|
||||
}
|
||||
if (llmModelT === "gpt-4-1106-preview") {
|
||||
return await modelProperties('Model.gpt-4-turbo', "gpt-4-1106-preview");
|
||||
}
|
||||
if (llmModelT === "claude-2") {
|
||||
return await modelProperties('Model.claude-2', "claude-2");
|
||||
}
|
||||
if (llmModelT === "xinghuo-2") {
|
||||
return await modelProperties('Model.xinghuo-2', "xinghuo-2");
|
||||
}
|
||||
if (llmModelT === "chatglm_pro") {
|
||||
return await modelProperties('Model.chatglm_pro', "chatglm_pro");
|
||||
}
|
||||
if (llmModelT === "ERNIE-Bot") {
|
||||
return await modelProperties('Model.ERNIE-Bot', "ERNIE-Bot");
|
||||
}
|
||||
if (llmModelT === "CodeLlama-34b-Instruct") {
|
||||
return await modelProperties('Model.CodeLlama-34b-Instruct', "CodeLlama-34b-Instruct");
|
||||
}
|
||||
if (llmModelT === "llama-2-70b-chat") {
|
||||
return await modelProperties('Model.llama-2-70b-chat', "llama-2-70b-chat");
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static getKeyType(apiKey: string): string | undefined {
|
||||
if (apiKey.startsWith("sk-")) {
|
||||
return "sk";
|
||||
} else if (apiKey.startsWith("DC.")) {
|
||||
return "DC";
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static async writeApiKeySecret(apiKey: string, llmType: string = "Unknow"): Promise<void> {
|
||||
await UiUtilWrapper.storeSecret(`Access_KEY_${llmType}`, apiKey);
|
||||
}
|
||||
static async loadApiKeySecret(llmType: string = "Unknow"): Promise<string | undefined> {
|
||||
return await UiUtilWrapper.secretStorageGet(`Access_KEY_${llmType}`);
|
||||
}
|
||||
|
||||
// get some provider's api key
|
||||
static async getProviderApiKey(provider: string): Promise<string | undefined> {
|
||||
// read key from configration first
|
||||
const providerProperty = `Provider.${provider}`;
|
||||
const providerConfig = UiUtilWrapper.getConfiguration("devchat", providerProperty);
|
||||
if (providerConfig) {
|
||||
if (providerConfig["access_key"]) {
|
||||
return providerConfig["access_key"];
|
||||
}
|
||||
}
|
||||
|
||||
const providerName = this.toProviderKey(provider);
|
||||
if (!providerName) {
|
||||
return undefined;
|
||||
}
|
||||
return await this.loadApiKeySecret(providerName);
|
||||
}
|
||||
|
||||
// get some provider's api base
|
||||
static async getProviderApiBase(provider: string): Promise<string | undefined> {
|
||||
// read key from configration first
|
||||
const providerProperty = `Provider.${provider}`;
|
||||
const providerConfig = UiUtilWrapper.getConfiguration("devchat", providerProperty);
|
||||
if (providerConfig) {
|
||||
if (providerConfig["api_base"]) {
|
||||
return providerConfig["api_base"];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
import { logger } from "./logger";
|
||||
|
||||
|
||||
type Action = {
|
||||
action: "delete" | "insert" | "modify";
|
||||
content?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
insert_after?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
insert_before?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
original_content?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
new_content?: string;
|
||||
};
|
||||
|
||||
|
||||
function findMatchingIndex(list1: string[], list2: string[]): number[] {
|
||||
logger.channel()?.info(`findMatchingIndex start: ${list2.join('\n')}`);
|
||||
|
||||
const matchingIndexes: number[] = [];
|
||||
for (let i = 0; i <= list1.length - list2.length; i++) {
|
||||
let isMatch = true;
|
||||
for (let j = 0; j < list2.length; j++) {
|
||||
if (list1[i + j].trim() !== list2[j].trim()) {
|
||||
|
||||
//if (j > 0) {
|
||||
logger.channel()?.info(`findMatchingIndex end at ${j} ${list1[i + j].trim()} != ${list2[j].trim()}`);
|
||||
//}
|
||||
|
||||
isMatch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isMatch) {
|
||||
matchingIndexes.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
logger.channel()?.info(`findMatchingIndex result: ${matchingIndexes.join(' ')}`);
|
||||
return matchingIndexes;
|
||||
}
|
||||
|
||||
export function applyCodeChanges(originalCode: string, actionsString: string): string {
|
||||
const actions = JSON.parse(actionsString) as Array<Action>;
|
||||
|
||||
const lines = originalCode.split('\n');
|
||||
// 构建与lines等长的数组,用于记录哪些行被修改过
|
||||
const modifiedIndexes: number[] = new Array(lines.length).fill(0);
|
||||
|
||||
// 构建子函数,用于在多个匹配索引中找出最优的索引
|
||||
const findOptimalMatchingIndex = (matchingIndexList: number[]) => {
|
||||
// 优先找出未被修改过的索引
|
||||
const optimalMatchingIndex = matchingIndexList.find(index => modifiedIndexes[index] === 0);
|
||||
// 如果所有索引都被修改过,则找出第一个索引
|
||||
if (optimalMatchingIndex === undefined) {
|
||||
if (matchingIndexList.length > 0) {
|
||||
return matchingIndexList[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return optimalMatchingIndex;
|
||||
};
|
||||
|
||||
for (const action of actions) {
|
||||
const contentLines = action.content?.split('\n') || [];
|
||||
const insertAfterLines = action.insert_after?.split('\n') || [];
|
||||
const insertBeforeLines = action.insert_before?.split('\n') || [];
|
||||
const originalContentLines = action.original_content?.split('\n') || [];
|
||||
|
||||
switch (action.action) {
|
||||
case 'delete':
|
||||
// find the matching index
|
||||
const matchingIndexList = findMatchingIndex(lines, contentLines);
|
||||
const optimalMatchingIndex = findOptimalMatchingIndex(matchingIndexList);
|
||||
if (matchingIndexList.length > 0) {
|
||||
if (optimalMatchingIndex !== undefined) {
|
||||
lines.splice(optimalMatchingIndex, contentLines.length);
|
||||
// 同步删除modifiedIndexes中记录
|
||||
modifiedIndexes.splice(optimalMatchingIndex, contentLines.length);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'insert':
|
||||
// find the matching index
|
||||
if (insertBeforeLines.length > 0) {
|
||||
const matchingIndexList1 = findMatchingIndex(lines, insertBeforeLines);
|
||||
const optimalMatchingIndex1 = findOptimalMatchingIndex(matchingIndexList1);
|
||||
|
||||
if (matchingIndexList1.length > 0) {
|
||||
if (optimalMatchingIndex1 !== undefined) {
|
||||
lines.splice(optimalMatchingIndex1, 0, ...contentLines);
|
||||
// 同步modifiedIndexes添加记录
|
||||
modifiedIndexes.splice(optimalMatchingIndex1, 0, ...new Array(contentLines.length).fill(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (insertAfterLines.length > 0) {
|
||||
const matchingIndexList2 = findMatchingIndex(lines, insertAfterLines);
|
||||
const optimalMatchingIndex2 = findOptimalMatchingIndex(matchingIndexList2);
|
||||
|
||||
if (matchingIndexList2.length > 0) {
|
||||
if (optimalMatchingIndex2 !== undefined) {
|
||||
lines.splice(optimalMatchingIndex2 + insertAfterLines.length, 0, ...contentLines);
|
||||
// 同步modifiedIndexes添加记录
|
||||
modifiedIndexes.splice(optimalMatchingIndex2 + insertAfterLines.length, 0, ...new Array(contentLines.length).fill(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'modify':
|
||||
// find the matching index
|
||||
const matchingIndexList3 = findMatchingIndex(lines, originalContentLines);
|
||||
const optimalMatchingIndex3 = findOptimalMatchingIndex(matchingIndexList3);
|
||||
|
||||
if (matchingIndexList3.length > 0) {
|
||||
if (optimalMatchingIndex3 !== undefined) {
|
||||
lines.splice(optimalMatchingIndex3, originalContentLines.length, ...action.new_content!.split('\n'));
|
||||
// 同步modifiedIndexes添加记录
|
||||
modifiedIndexes.splice(optimalMatchingIndex3, originalContentLines.length, ...new Array(action.new_content!.split('\n').length).fill(1));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
|
||||
export function isValidActionString(actionString: string): boolean {
|
||||
try {
|
||||
const actions = JSON.parse(actionString) as Array<Action>;
|
||||
|
||||
for (const action of actions) {
|
||||
if (!["delete", "insert", "modify"].includes(action.action)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.action === "delete" && !action.content) {
|
||||
logger.channel()?.error(`Invalid action string: ${action}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.action === "insert" && (!action.content || (!action.insert_after && !action.insert_before))) {
|
||||
logger.channel()?.error(`Invalid action string: ${action}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.action === "modify" && (!action.original_content || !action.new_content)) {
|
||||
logger.channel()?.error(`Invalid action string: ${action}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
Util for askCode
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { UiUtilWrapper } from './uiUtil';
|
||||
|
||||
let indexingStatus = 'stopped'; // 'started' | 'indexing' | 'stopped'
|
||||
|
||||
export function updateLastModifyTime() {
|
||||
const workspaceFolder = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
if (!workspaceFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
let files = fs.readdirSync(workspaceFolder).filter(file => !file.startsWith('.'));
|
||||
|
||||
let lastModifyTime = {};
|
||||
for (let file of files) {
|
||||
let stats = fs.statSync(path.join(workspaceFolder, file));
|
||||
lastModifyTime[file] = stats.mtime.toUTCString();
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(workspaceFolder, '.chat', '.lastModifyTime.json'), JSON.stringify(lastModifyTime));
|
||||
}
|
||||
|
||||
export function isNeedIndexingCode() {
|
||||
const workspaceFolder = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
if (!workspaceFolder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let lastModifyTimeFile = path.join(workspaceFolder, '.chat', '.lastModifyTime.json');
|
||||
if (!fs.existsSync(lastModifyTimeFile)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let files = fs.readdirSync(workspaceFolder).filter(file => !file.startsWith('.'));
|
||||
|
||||
// load lastModifyTime from .chat/.lastModifyTime.json
|
||||
let lastModifyTime = {};
|
||||
if (fs.existsSync(lastModifyTimeFile)) {
|
||||
lastModifyTime = JSON.parse(fs.readFileSync(lastModifyTimeFile, 'utf-8'));
|
||||
}
|
||||
|
||||
for (let file of files) {
|
||||
let stats = fs.statSync(path.join(workspaceFolder, file));
|
||||
if (!lastModifyTime[file] || stats.mtime.toUTCString() !== lastModifyTime[file]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (Object.keys(lastModifyTime).length !== files.length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function updateIndexingStatus(status: string) {
|
||||
if (status === "started") {
|
||||
updateLastModifyTime();
|
||||
}
|
||||
indexingStatus = status;
|
||||
}
|
||||
|
||||
export function isIndexingStopped() {
|
||||
return indexingStatus === 'stopped';
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
|
||||
export function assertValue(value: any, message: string) {
|
||||
if (value) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
@ -1,334 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as yaml from 'yaml';
|
||||
import * as vscode from 'vscode';
|
||||
import * as childProcess from 'child_process';
|
||||
|
||||
import { parseArgsStringToArgv } from 'string-argv';
|
||||
|
||||
import { logger } from './logger';
|
||||
import { spawn, exec } from 'child_process';
|
||||
import { UiUtilWrapper } from './uiUtil';
|
||||
import { ApiKeyManager } from './apiKey';
|
||||
var kill = require('tree-kill');
|
||||
|
||||
export async function saveModelSettings(): Promise<void> {
|
||||
// support models
|
||||
const supportModels = {
|
||||
"Model.gpt-3-5": "gpt-3.5-turbo",
|
||||
"Model.gpt-3-5-1106": "gpt-3.5-turbo-1106",
|
||||
"Model.gpt-3-5-16k": "gpt-3.5-turbo-16k",
|
||||
"Model.gpt-4": "gpt-4",
|
||||
"Model.gpt-4-turbo": "gpt-4-1106-preview",
|
||||
"Model.claude-2": "claude-2",
|
||||
"Model.xinghuo-2": "xinghuo-2",
|
||||
"Model.chatglm_pro": "chatglm_pro",
|
||||
"Model.ERNIE-Bot": "ERNIE-Bot",
|
||||
"Model.CodeLlama-34b-Instruct": "CodeLlama-34b-Instruct",
|
||||
"Model.llama-2-70b-chat": "llama-2-70b-chat"
|
||||
};
|
||||
|
||||
// is enable stream
|
||||
const openaiStream = UiUtilWrapper.getConfiguration('DevChat', 'OpenAI.stream');
|
||||
|
||||
let devchatConfig = {};
|
||||
for (const model of Object.keys(supportModels)) {
|
||||
const modelConfig = UiUtilWrapper.getConfiguration('devchat', model);
|
||||
if (modelConfig) {
|
||||
devchatConfig[supportModels[model]] = {
|
||||
"stream": openaiStream
|
||||
};
|
||||
for (const key of Object.keys(modelConfig || {})) {
|
||||
const property = modelConfig![key];
|
||||
devchatConfig[supportModels[model]][key] = property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let devchatModels = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
"default_model": "gpt-3.5-turbo-16k",
|
||||
"models": devchatConfig
|
||||
};
|
||||
|
||||
// write to config file
|
||||
const os = process.platform;
|
||||
const userHome = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME;
|
||||
|
||||
const configPath = path.join(userHome!, '.chat', 'config.yml');
|
||||
// write devchatConfig to configPath
|
||||
const yamlString = yaml.stringify(devchatModels);
|
||||
fs.writeFileSync(configPath, yamlString);
|
||||
}
|
||||
|
||||
async function createOpenAiKeyEnv() {
|
||||
let envs = {...process.env};
|
||||
const llmModelData = await ApiKeyManager.llmModel();
|
||||
if (llmModelData && llmModelData.api_key) {
|
||||
envs['OPENAI_API_KEY'] = llmModelData.api_key;
|
||||
}
|
||||
|
||||
const openAiApiBase = llmModelData.api_base;
|
||||
if (openAiApiBase) {
|
||||
envs['OPENAI_API_BASE'] = openAiApiBase;
|
||||
}
|
||||
|
||||
return envs;
|
||||
}
|
||||
export function createTempSubdirectory(subdir: string): string {
|
||||
// 获取系统临时目录
|
||||
const tempDir = os.tmpdir();
|
||||
// 构建完整的目录路径
|
||||
let targetDir = path.join(tempDir, subdir, Date.now().toString());
|
||||
// 检查目录是否存在,如果存在则重新生成目录名称
|
||||
while (fs.existsSync(targetDir)) {
|
||||
targetDir = path.join(tempDir, subdir, Date.now().toString());
|
||||
}
|
||||
// 递归创建目录
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
// 返回创建的目录的绝对路径
|
||||
return targetDir;
|
||||
}
|
||||
|
||||
export interface CommandResult {
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
export class CommandRun {
|
||||
private childProcess: any;
|
||||
private _input: string;
|
||||
|
||||
// init childProcess in construction function
|
||||
constructor() {
|
||||
this._input = "";
|
||||
this.childProcess = null;
|
||||
}
|
||||
|
||||
public async spawnAsync(command: string, args: string[], options: object, onData: ((data: string) => void) | undefined, onError: ((data: string) => void) | undefined, onOutputFile: ((command: string, stdout: string, stderr: string) => string) | undefined, outputFile: string | undefined): Promise<CommandResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.channel()?.info(`Running command: ${command} ${args.join(' ')}`);
|
||||
this._input = "";
|
||||
const argsNew: string[] = args.map((arg) => {
|
||||
if (arg.trim()[0] === '$') {
|
||||
// get rest string except '$'
|
||||
const restStr = arg.trim().slice(1);
|
||||
if (process.env[restStr]) {
|
||||
return process.env[restStr]!;
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
} else {
|
||||
return arg;
|
||||
}
|
||||
});
|
||||
|
||||
this.childProcess = spawn(command, argsNew, options);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
this.childProcess.stdout.on('data', (data: { toString: () => any; }) => {
|
||||
const dataStr = this._input + data.toString();
|
||||
this._input = "";
|
||||
if (onData) {
|
||||
onData(dataStr);
|
||||
}
|
||||
stdout += dataStr;
|
||||
});
|
||||
|
||||
this.childProcess.stderr.on('data', (data: string) => {
|
||||
const dataStr = data.toString();
|
||||
if (onError) {
|
||||
onError(dataStr);
|
||||
}
|
||||
stderr += dataStr;
|
||||
});
|
||||
|
||||
this.childProcess.on('close', (code: number) => {
|
||||
let outputData = stdout;
|
||||
if (onOutputFile) {
|
||||
outputData = onOutputFile(command + " " + args.join(" "), stdout, stderr);
|
||||
}
|
||||
|
||||
if (outputFile) {
|
||||
fs.writeFileSync(outputFile, outputData);
|
||||
}
|
||||
|
||||
if (stderr && !onError) {
|
||||
logger.channel()?.error(stderr);
|
||||
logger.channel()?.show();
|
||||
}
|
||||
|
||||
this.childProcess = null;
|
||||
if (code === 0) {
|
||||
resolve({ exitCode: code, stdout, stderr });
|
||||
} else {
|
||||
resolve({ exitCode: code, stdout, stderr });
|
||||
}
|
||||
});
|
||||
|
||||
// Add error event listener to handle command not found exception
|
||||
this.childProcess.on('error', (error: any) => {
|
||||
this.childProcess = null;
|
||||
let errorMessage = error.message;
|
||||
if (error.code === 'ENOENT') {
|
||||
errorMessage = `Command not found: ${command}`;
|
||||
logger.channel()?.error(`Command "${command}" not found`);
|
||||
logger.channel()?.show();
|
||||
} else {
|
||||
logger.channel()?.error(`Error: ${error.message}`);
|
||||
logger.channel()?.show();
|
||||
}
|
||||
resolve({ exitCode: error.code, stdout: "", stderr: errorMessage });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
public write(input: string) {
|
||||
if (this.childProcess) {
|
||||
this._input += input;
|
||||
this.childProcess.stdin.write(input);
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.childProcess) {
|
||||
kill(this.childProcess.pid, 'SIGKILL', (err) => {
|
||||
if (err) {
|
||||
logger.channel()?.error('Failed to kill process tree:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runCommandAndWriteOutput(
|
||||
command: string,
|
||||
args: string[],
|
||||
outputFile: string | undefined
|
||||
): Promise<CommandResult> {
|
||||
const run = new CommandRun();
|
||||
const options = {
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.',
|
||||
env: await createOpenAiKeyEnv()
|
||||
};
|
||||
|
||||
return run.spawnAsync(command, args, options, undefined, undefined, undefined, outputFile);
|
||||
}
|
||||
|
||||
export async function runCommandStringAndWriteOutput(
|
||||
commandString: string,
|
||||
outputFile: string | undefined
|
||||
): Promise<CommandResult> {
|
||||
const run = new CommandRun();
|
||||
const options = {
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.',
|
||||
env: await createOpenAiKeyEnv()
|
||||
};
|
||||
|
||||
// Split the commandString into command and args array using string-argv
|
||||
const commandParts = parseArgsStringToArgv(commandString);
|
||||
const command = commandParts[0];
|
||||
const args = commandParts.slice(1);
|
||||
|
||||
const onOutputFile = (command: string, stdout: string, stderr: string): string => {
|
||||
const data = {
|
||||
command: commandString,
|
||||
content: stdout,
|
||||
};
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
|
||||
return run.spawnAsync(command, args, options, undefined, undefined, onOutputFile, outputFile);
|
||||
}
|
||||
|
||||
export async function runCommandStringArrayAndWriteOutput(
|
||||
commandStringList: string[],
|
||||
outputFile: string
|
||||
): Promise<CommandResult> {
|
||||
const run = new CommandRun();
|
||||
const options = {
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.',
|
||||
env: await createOpenAiKeyEnv()
|
||||
};
|
||||
|
||||
const commandString = commandStringList[0];
|
||||
const args: string[] = commandStringList.slice(1);
|
||||
const onOutputFile = (command: string, stdout: string, stderr: string): string => {
|
||||
const data = {
|
||||
command: commandString,
|
||||
content: stdout,
|
||||
};
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
|
||||
return run.spawnAsync(commandString, args, options, undefined, undefined, onOutputFile, outputFile);
|
||||
}
|
||||
|
||||
export async function getLanguageIdByFileName(fileName: string): Promise<string | undefined> {
|
||||
try {
|
||||
const languageId = await UiUtilWrapper.languageId(fileName);
|
||||
return languageId;
|
||||
} catch (error) {
|
||||
// 如果无法打开文件或发生其他错误,返回undefined
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function runCommand(command: string): string {
|
||||
return childProcess.execSync(command).toString();
|
||||
}
|
||||
|
||||
export function runCommandStringAndWriteOutputSync(command: string, outputFile: string): CommandResult {
|
||||
try {
|
||||
const options = {
|
||||
cwd: UiUtilWrapper.workspaceFoldersFirstPath() || '.'
|
||||
};
|
||||
const output = childProcess.execSync(command, options).toString();
|
||||
const onOutputFile = (command: string, stdout: string): string => {
|
||||
const data = {
|
||||
"command": command,
|
||||
"content": stdout,
|
||||
};
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
fs.writeFileSync(outputFile, onOutputFile(command, output));
|
||||
return { exitCode: 0, stdout: output, stderr: '' };
|
||||
} catch (error) {
|
||||
logger.channel()?.error(`Error occurred: ${error}`);
|
||||
logger.channel()?.show();
|
||||
return { exitCode: 1, stdout: '', stderr: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export function gitLsTree(withAbsolutePath: boolean = false): string[] {
|
||||
// Run the git ls-tree command
|
||||
const workspacePath = UiUtilWrapper.workspaceFoldersFirstPath() || '.';
|
||||
const result = childProcess.execSync('git ls-tree -r --name-only HEAD', {
|
||||
cwd: workspacePath,
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
|
||||
// Split the result into lines
|
||||
const lines = result.split('\n');
|
||||
|
||||
// Remove the last line if it is empty
|
||||
if (lines[lines.length - 1] === '') {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
// Return the lines
|
||||
// Convert the lines to absolute paths
|
||||
if (withAbsolutePath) {
|
||||
const absolutePaths = lines.map(line => path.resolve(workspacePath, line));
|
||||
|
||||
// Return the absolute paths
|
||||
return absolutePaths;
|
||||
} else {
|
||||
return lines;
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
export class FilePairManager {
|
||||
private static instance: FilePairManager;
|
||||
private filePairs: Map<string, [string, string]>;
|
||||
|
||||
private constructor() {
|
||||
this.filePairs = new Map<string, [string, string]>();
|
||||
}
|
||||
|
||||
static getInstance(): FilePairManager {
|
||||
if (!FilePairManager.instance) {
|
||||
FilePairManager.instance = new FilePairManager();
|
||||
}
|
||||
return FilePairManager.instance;
|
||||
}
|
||||
|
||||
addFilePair(file1: string, file2: string): void {
|
||||
this.filePairs.set(file1, [file1, file2]);
|
||||
this.filePairs.set(file2, [file1, file2]);
|
||||
}
|
||||
|
||||
findPair(file: string): [string, string] | undefined {
|
||||
return this.filePairs.get(file);
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DevChatViewProvider } from '../panel/devchatView';
|
||||
|
||||
export class ExtensionContextHolder {
|
||||
private static _context: vscode.ExtensionContext | undefined;
|
||||
private static _provider: DevChatViewProvider | undefined;
|
||||
|
||||
static set context(context: vscode.ExtensionContext | undefined) {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
static get context(): vscode.ExtensionContext | undefined {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
static set provider(provider: DevChatViewProvider | undefined) {
|
||||
this._provider = provider;
|
||||
}
|
||||
|
||||
static get provider(): DevChatViewProvider | undefined {
|
||||
return this._provider;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const featureTogglesJson = `
|
||||
{
|
||||
"ask-code-summary": false,
|
||||
"ask-code": true,
|
||||
"ask-code-dfs": false
|
||||
}`;
|
||||
const featureToggles = JSON.parse(featureTogglesJson);
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export function FT(feature: string): boolean {
|
||||
const betaInvitationCode = vscode.workspace.getConfiguration('DevChat').get<string>('betaInvitationCode');
|
||||
const expectedInvitationCode = 'WELCOMEADDTODEVCHAT';
|
||||
|
||||
return betaInvitationCode === expectedInvitationCode || featureToggles[feature] === true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export function FTs(): any {
|
||||
// visited features
|
||||
let newFeatureToggles = {};
|
||||
for (const feature in featureToggles) {
|
||||
newFeatureToggles[feature] = FT(feature);
|
||||
}
|
||||
return newFeatureToggles;
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
|
||||
总结如下:
|
||||
|
||||
在VSCode插件开发中,我们讨论了如何实现内测功能的激活控制。主要有以下几种方式:
|
||||
|
||||
1. **使用特定的配置项**:在插件的配置项中添加一个特定的字段,例如`enableBetaFeatures`,根据这个配置项的值来决定是否启用内测功能。
|
||||
|
||||
2. **使用特定的命令**:在插件中添加一个特定的命令,例如`activateBetaFeatures`,只有当用户执行了这个命令,才启用内测功能。
|
||||
|
||||
3. **使用许可证**:在插件中添加一个许可证验证功能,只有当用户输入了有效的许可证,才启用内测功能。
|
||||
|
||||
4. **使用特定的版本**:在插件的版本号中添加一个特定的标识,例如`1.0.0-beta`,只有当插件的版本号包含了这个标识,才启用内测功能。
|
||||
|
||||
为了避免在内测结束后需要修改代码,我们推荐使用特性开关(Feature Toggles)的方式来管理内测功能。可以创建一个特性开关的配置文件,例如`feature-toggles.json`,在这个文件中定义哪些特性是开启的,哪些特性是关闭的。在插件代码中,读取这个配置文件,根据特性开关的值来决定是否启用某个特性。
|
||||
|
||||
对于`package.json`文件中定义的命令,我们可以在插件的激活函数(`activate`函数)中动态地注册或注销命令来实现这个功能。首先,在`package.json`文件中定义所有可能需要的命令,包括内测命令。然后,在`activate`函数中,根据特性开关的值来决定是否注册内测命令。
|
||||
|
||||
以上就是我们对于VSCode插件开发中内测功能激活控制的讨论总结。
|
@ -1,21 +0,0 @@
|
||||
|
||||
export interface LogChannel {
|
||||
info(message: string, ...args: any[]): void;
|
||||
warn(message: string, ...args: any[]): void;
|
||||
error(message: string | Error, ...args: any[]): void;
|
||||
debug(message: string, ...args: any[]): void;
|
||||
show(): void;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export class logger {
|
||||
private static _channel: LogChannel | undefined;
|
||||
public static init(channel: LogChannel): void {
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
public static channel(): LogChannel | undefined {
|
||||
return this._channel;
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
import { LogChannel } from "./logger";
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class LoggerChannelVscode implements LogChannel {
|
||||
_channel: vscode.LogOutputChannel;
|
||||
|
||||
private static _instance: LoggerChannelVscode;
|
||||
|
||||
private constructor() {
|
||||
this._channel = vscode.window.createOutputChannel('DevChat', { log: true });
|
||||
}
|
||||
|
||||
public static getInstance(): LoggerChannelVscode {
|
||||
if (!this._instance) {
|
||||
this._instance = new LoggerChannelVscode();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
info(message: string, ...args: any[]): void {
|
||||
this._channel.info(message, ...args);
|
||||
}
|
||||
|
||||
warn(message: string, ...args: any[]): void {
|
||||
this._channel.warn(message, ...args);
|
||||
}
|
||||
|
||||
error(message: string | Error, ...args: any[]): void {
|
||||
this._channel.error(message, ...args);
|
||||
}
|
||||
|
||||
debug(message: string, ...args: any[]): void {
|
||||
this._channel.debug(message, ...args);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
this._channel.show();
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
|
||||
export class MessageHistory {
|
||||
private history: any[];
|
||||
private lastmessage: any | null;
|
||||
private topic: string | null;
|
||||
|
||||
constructor() {
|
||||
this.history = [];
|
||||
this.lastmessage = null;
|
||||
this.topic = null;
|
||||
}
|
||||
|
||||
setTopic(topic: string) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
getTopic() {
|
||||
return this.topic;
|
||||
}
|
||||
|
||||
add(message: any) {
|
||||
this.history.push(message);
|
||||
this.lastmessage = message;
|
||||
}
|
||||
|
||||
getList() {
|
||||
return this.history;
|
||||
}
|
||||
|
||||
find(hash: string) {
|
||||
return this.history.find(message => message.hash === hash);
|
||||
}
|
||||
|
||||
delete(hash: string) {
|
||||
const index = this.history.findIndex(message => message.hash === hash);
|
||||
if (index >= 0) {
|
||||
this.history.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.lastmessage?.hash === hash) {
|
||||
this.lastmessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
findLast() {
|
||||
return this.lastmessage;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.history = [];
|
||||
this.lastmessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
const messageHistory = new MessageHistory();
|
||||
export default messageHistory;
|
@ -1,50 +0,0 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class ProgressBar {
|
||||
private message: string;
|
||||
private error: string | undefined;
|
||||
private finish: boolean | undefined;
|
||||
|
||||
constructor() {
|
||||
this.message = "";
|
||||
}
|
||||
|
||||
init() {
|
||||
vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: 'DevChat',
|
||||
cancellable: false
|
||||
}, (progress, token) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const timer = setInterval(() => {
|
||||
if (this.finish === true && this.error === "") {
|
||||
vscode.window.showInformationMessage(`${this.message}`);
|
||||
resolve();
|
||||
clearInterval(timer);
|
||||
} else if (this.finish === true && this.error !== "") {
|
||||
vscode.window.showErrorMessage(`Indexing failed: ${this.error}`);
|
||||
resolve();
|
||||
clearInterval(timer);
|
||||
} else if (this.finish !== true) {
|
||||
progress.report({ message: `${this.message}` });
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
update(message: string, increment?: number) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
end() {
|
||||
this.error = "";
|
||||
this.finish = true;
|
||||
}
|
||||
|
||||
endWithError(error: string) {
|
||||
this.error = error;
|
||||
this.finish = true;
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
Install devchat
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { installConda } from "./conda_install";
|
||||
import { getMicromambaUrl } from "./conda_url";
|
||||
import { installPackage } from "./package_install";
|
||||
import { installPython, installPythonMicromamba } from "./python_install";
|
||||
|
||||
// step 1. install conda
|
||||
// step 2. create env with python 3.11.4
|
||||
// step 3. install devchat in the env
|
||||
|
||||
|
||||
export async function createEnvByMamba(pkgName: string, pkgVersion: string, pythonVersion: string) : Promise<string> {
|
||||
logger.channel()?.info('Find micromamba ...');
|
||||
const mambaCommand = getMicromambaUrl();
|
||||
logger.channel()?.info('micromamba url: ' + mambaCommand);
|
||||
|
||||
// create env with specify python
|
||||
logger.channel()?.info('Create env ...');
|
||||
let pythonCommand = '';
|
||||
// try 3 times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
pythonCommand = await installPythonMicromamba(mambaCommand, pkgName, pythonVersion);
|
||||
if (pythonCommand) {
|
||||
break;
|
||||
}
|
||||
} catch(error) {
|
||||
logger.channel()?.info(`Exception: ${error}`);
|
||||
}
|
||||
|
||||
logger.channel()?.info(`Create env failed, try again: ${i + 1}`);
|
||||
}
|
||||
|
||||
return pythonCommand;
|
||||
}
|
||||
|
||||
export async function createEnvByConda(pkgName: string, pkgVersion: string, pythonVersion: string) : Promise<string> {
|
||||
// install conda
|
||||
logger.channel()?.info('Install conda ...');
|
||||
const condaCommand = await installConda();
|
||||
if (!condaCommand) {
|
||||
logger.channel()?.error('Install conda failed');
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
|
||||
// create env with specify python
|
||||
logger.channel()?.info('Create env ...');
|
||||
let pythonCommand = '';
|
||||
// try 3 times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
pythonCommand = await installPython(condaCommand, pkgName, pythonVersion);
|
||||
if (pythonCommand) {
|
||||
break;
|
||||
}
|
||||
} catch(error) {
|
||||
logger.channel()?.info(`Exception: ${error}`);
|
||||
}
|
||||
|
||||
logger.channel()?.info(`Create env failed, try again: ${i + 1}`);
|
||||
}
|
||||
|
||||
return pythonCommand;
|
||||
}
|
||||
|
||||
export async function appInstall(pkgName: string, pkgVersion: string, pythonVersion: string) : Promise<string> {
|
||||
logger.channel()?.info(`create env for python ...`);
|
||||
logger.channel()?.info(`try to create env by mamba ...`);
|
||||
let pythonCommand = await createEnvByMamba(pkgName, pkgVersion, pythonVersion);
|
||||
|
||||
if (!pythonCommand || pythonCommand === "") {
|
||||
logger.channel()?.info(`create env by mamba failed, try to create env by conda ...`);
|
||||
pythonCommand = await createEnvByConda(pkgName, pkgVersion, pythonVersion);
|
||||
}
|
||||
|
||||
if (!pythonCommand) {
|
||||
logger.channel()?.error('Create env failed');
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
logger.channel()?.info(`Create env success: ${pythonCommand}`);
|
||||
|
||||
// install devchat in the env
|
||||
logger.channel()?.info('Install python packages ...');
|
||||
let isInstalled = false;
|
||||
// try 3 times
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let otherSource: string | undefined = undefined;
|
||||
if (i>1) {
|
||||
otherSource = 'https://pypi.tuna.tsinghua.edu.cn/simple/';
|
||||
}
|
||||
isInstalled = await installPackage(pythonCommand, pkgName + pkgVersion, otherSource);
|
||||
if (isInstalled) {
|
||||
break;
|
||||
}
|
||||
logger.channel()?.info(`Install packages failed, try again: ${i + 1}`);
|
||||
}
|
||||
if (!isInstalled) {
|
||||
logger.channel()?.error('Install packages failed');
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
|
||||
return pythonCommand;
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
/*
|
||||
Install conda command
|
||||
Install file from https://repo.anaconda.com/miniconda/
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { getCondaDownloadUrl } from "./conda_url";
|
||||
import { downloadFile } from "./https_download";
|
||||
|
||||
import { exec, spawn } from 'child_process';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
||||
// Check whether conda has installed before installing conda.
|
||||
// If "conda -V" runs ok, then conda has installed.
|
||||
// If ~/.devchat/conda/bin/conda exists, then conda has installed.
|
||||
|
||||
// is "conda -V" ok? then find conda command
|
||||
// is ~/.devchat/conda/bin/conda exists? then return ~/.devchat/conda/bin/conda
|
||||
// find conda command by: with different os use diffenc command: which conda | where conda
|
||||
async function isCondaInstalled(): Promise<string> {
|
||||
// whether conda -V runs ok
|
||||
const condaVersion = await runCommand('conda -V');
|
||||
if (condaVersion) {
|
||||
// find conda command by: with different os use diffenc command: which conda | where conda
|
||||
const os = process.platform;
|
||||
const command = os === 'win32' ? 'where conda' : 'which conda';
|
||||
const condaCommand = await runCommand(command);
|
||||
if (condaCommand) {
|
||||
const condaCommandLines = condaCommand.split('\n');
|
||||
return condaCommandLines[0].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// whether ~/.devchat/conda/bin/conda exists
|
||||
const os = process.platform;
|
||||
const userHome = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME;
|
||||
const pathToConda = `${userHome}/.chat/conda`;
|
||||
const condaPath = os === 'win32' ? `${pathToConda}/Scripts/conda.exe` : `${pathToConda}/bin/conda`;
|
||||
logger.channel()?.info(`checking conda path: ${condaPath}`);
|
||||
const isCondaPathExists = fs.existsSync(condaPath);
|
||||
if (isCondaPathExists) {
|
||||
return condaPath;
|
||||
}
|
||||
|
||||
logger.channel()?.info(`conda path: ${condaPath} not exists`);
|
||||
return '';
|
||||
}
|
||||
|
||||
function runCommand(command: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve('');
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function checkPathExists(path: string): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`test -e ${path}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// install file is an exe file or sh file
|
||||
// according to different os, use different command to install conda, Installing in silent mode
|
||||
// install conda to USER_HOME/.devchat/conda
|
||||
// return: conda command path
|
||||
async function installCondaByInstallFile(installFileUrl: string) : Promise<string> {
|
||||
// Determine the operating system
|
||||
const os = process.platform;
|
||||
|
||||
// Set the installation directory for conda
|
||||
const userHome = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME;
|
||||
const pathToConda = `${userHome}/.chat/conda`;
|
||||
// if pathToConda has exist, remove it first
|
||||
try {
|
||||
if (fs.existsSync(pathToConda)) {
|
||||
fs.rmSync(pathToConda, { recursive: true, force: true });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.channel()?.error(`Error while deleting ${pathToConda}:`, error);
|
||||
}
|
||||
|
||||
// Define the command to install conda based on the operating system
|
||||
let command = '';
|
||||
if (os === 'win32') {
|
||||
const winPathToConda = pathToConda.replace(/\//g, '\\');
|
||||
command = `start /wait "" "${installFileUrl}" /InstallationType=JustMe /AddToPath=0 /RegisterPython=0 /S /D=${winPathToConda}`;
|
||||
} else if (os === 'linux') {
|
||||
command = `bash "${installFileUrl}" -b -p "${pathToConda}"`;
|
||||
} else if (os === 'darwin') {
|
||||
command = `bash "${installFileUrl}" -b -p "${pathToConda}"`;
|
||||
} else {
|
||||
throw new Error('Unsupported operating system');
|
||||
}
|
||||
|
||||
// Execute the command to install conda
|
||||
logger.channel()?.info(`install conda command: ${command}`);
|
||||
try {
|
||||
await executeCommand(command);
|
||||
|
||||
// Return the path to the conda command
|
||||
let condaCommandPath = '';
|
||||
if (os === 'win32') {
|
||||
condaCommandPath = `${pathToConda}\\Scripts\\conda.exe`;
|
||||
} else {
|
||||
condaCommandPath = `${pathToConda}/bin/conda`;
|
||||
}
|
||||
|
||||
return condaCommandPath;
|
||||
} catch(error) {
|
||||
logger.channel()?.error(`install conda failed: ${error}`);
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to execute a command
|
||||
function executeCommand(command: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.channel()?.error(`exec error: ${error}`);
|
||||
logger.channel()?.show();
|
||||
reject(error);
|
||||
} else {
|
||||
if (stderr) {
|
||||
logger.channel()?.error(`stderr: ${error}`);
|
||||
logger.channel()?.show();
|
||||
}
|
||||
if (stdout) {
|
||||
logger.channel()?.info(`${stdout}`);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function installConda() : Promise<string> {
|
||||
// step 1. check whether conda has installed
|
||||
// step 2. download install file
|
||||
// step 3. install conda by install file
|
||||
|
||||
const condaCommand = await isCondaInstalled();
|
||||
if (condaCommand) {
|
||||
logger.channel()?.info(`conda has installed: ${condaCommand}`);
|
||||
return condaCommand;
|
||||
}
|
||||
|
||||
const downloadInstallFile = getCondaDownloadUrl();
|
||||
if (!downloadInstallFile) {
|
||||
logger.channel()?.error(`get conda download url failed`);
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
|
||||
logger.channel()?.info(`conda download url: ${downloadInstallFile}`);
|
||||
let installFileLocal = '';
|
||||
// try 3 times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
installFileLocal = await downloadFile(downloadInstallFile);
|
||||
if (installFileLocal && installFileLocal !== '') {
|
||||
break;
|
||||
}
|
||||
logger.channel()?.info(`download conda install file failed, try again ...`);
|
||||
}
|
||||
if (!installFileLocal || installFileLocal === '') {
|
||||
logger.channel()?.error(`download conda install file failed`);
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
|
||||
logger.channel()?.info(`conda install file: ${installFileLocal}`);
|
||||
const installedConda = await installCondaByInstallFile(installFileLocal);
|
||||
return installedConda;
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
/*
|
||||
Get conda download url
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import { logger } from '../logger';
|
||||
import { UiUtilVscode } from '../uiUtil_vscode';
|
||||
import { UiUtilWrapper } from '../uiUtil';
|
||||
import path from 'path';
|
||||
|
||||
function getDownloadFileName(): string {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
logger.channel()?.info(`Platform: ${platform}, Arch: ${arch}`);
|
||||
|
||||
if (platform === "win32") {
|
||||
if (arch === "x64") {
|
||||
return "Miniconda3-latest-Windows-x86_64.exe";
|
||||
} else if (arch === "ia32") {
|
||||
return "Miniconda3-latest-Windows-x86.exe";
|
||||
} else {
|
||||
return "Miniconda3-latest-Windows-x86_64.exe";
|
||||
}
|
||||
} else if (platform === "darwin") {
|
||||
if (arch === "x64") {
|
||||
return "Miniconda3-latest-MacOSX-x86_64.sh";
|
||||
} else if (arch === "arm64") {
|
||||
return "Miniconda3-latest-MacOSX-arm64.sh";
|
||||
} else if (arch === "x86") {
|
||||
return "Miniconda3-latest-MacOSX-x86.sh";
|
||||
} else {
|
||||
return "Miniconda3-latest-MacOSX-arm64.sh";
|
||||
}
|
||||
} else if (platform === "linux") {
|
||||
if (arch === "x64") {
|
||||
return "Miniconda3-latest-Linux-x86_64.sh";
|
||||
} else if (arch === "s390x") {
|
||||
return "Miniconda3-latest-Linux-s390x.sh";
|
||||
} else if (arch === "ppc64le") {
|
||||
return "Miniconda3-latest-Linux-ppc64le.sh";
|
||||
} else if (arch === "aarch64") {
|
||||
return "Miniconda3-latest-Linux-aarch64.sh";
|
||||
} else if (arch === "x86") {
|
||||
return "Miniconda3-latest-Linux-x86.sh";
|
||||
} else if (arch === "armv7l") {
|
||||
return "Miniconda3-latest-Linux-armv7l.sh";
|
||||
} else {
|
||||
return "Miniconda3-latest-Linux-x86_64.sh";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getMicromambaUrl(): string {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
logger.channel()?.info(`Platform: ${platform}, Arch: ${arch}`);
|
||||
|
||||
let micromambaUrl = '';
|
||||
if (platform === "win32") {
|
||||
micromambaUrl = "micromamba-win-64";
|
||||
} else if (platform === "darwin") {
|
||||
if (arch === "arm64") {
|
||||
micromambaUrl = "micromamba-osx-arm64";
|
||||
} else if (arch === "x86" || arch === "x64") {
|
||||
micromambaUrl = "micromamba-osx-64";
|
||||
} else {
|
||||
micromambaUrl = "micromamba-osx-64";
|
||||
}
|
||||
} else if (platform === "linux") {
|
||||
if (arch === "x64") {
|
||||
micromambaUrl = "micromamba-linux-64";
|
||||
} else if (arch === "ppc64le") {
|
||||
micromambaUrl = "micromamba-linux-ppc64le";
|
||||
} else if (arch === "aarch64") {
|
||||
micromambaUrl = "micromamba-linux-aarch64";
|
||||
} else {
|
||||
micromambaUrl = "micromamba-linux-64";
|
||||
}
|
||||
}
|
||||
|
||||
const micromambaPath = path.join(UiUtilWrapper.extensionPath(), 'tools', micromambaUrl, "bin", "micromamba");
|
||||
return micromambaPath;
|
||||
}
|
||||
|
||||
export function getCondaDownloadUrl(): string {
|
||||
return 'https://repo.anaconda.com/miniconda/' + getDownloadFileName();
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { logger } from '../logger';
|
||||
|
||||
// download url to tmp directory
|
||||
// return: local file path or empty string
|
||||
export async function downloadFile(url: string): Promise<string> {
|
||||
const os = process.platform;
|
||||
const tempDir = os === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME;
|
||||
|
||||
const fileName = path.basename(url); // 从 URL 中提取文件名称
|
||||
const destination = path.join(tempDir!, fileName); // 构建文件路径
|
||||
|
||||
const file = fs.createWriteStream(destination);
|
||||
let downloadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
let lastProgress = 0;
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
https.get(url, (response) => {
|
||||
totalBytes = parseInt(response.headers['content-length'] || '0', 10);
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloadedBytes += chunk.length;
|
||||
const progress = (downloadedBytes / totalBytes) * 100;
|
||||
|
||||
if (progress - lastProgress >= 3) {
|
||||
logger.channel()?.info(`Downloaded ${downloadedBytes} bytes (${progress.toFixed(2)}%)`);
|
||||
lastProgress = progress;
|
||||
}
|
||||
});
|
||||
|
||||
response.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
if (downloadedBytes !== totalBytes) {
|
||||
resolve('');
|
||||
} else {
|
||||
resolve(destination);
|
||||
}
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
fs.unlink(destination, () => {
|
||||
resolve('');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
/*
|
||||
Install DevChat with python=3.11.4
|
||||
*/
|
||||
|
||||
import { logger } from "../logger";
|
||||
import { appInstall, createEnvByConda, createEnvByMamba } from "./app_install";
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { UiUtilWrapper } from "../uiUtil";
|
||||
import { getValidPythonCommand } from "../../contributes/commandsBase";
|
||||
|
||||
|
||||
let isDevChatInstalling: boolean | undefined = undefined;
|
||||
|
||||
export function isDevchatInstalling(): boolean {
|
||||
if (isDevChatInstalling === true) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// python version: 3.11.4
|
||||
// pkg name: devchat
|
||||
// return: path to devchat, devchat is located in the same directory as python
|
||||
export async function installDevchat(): Promise<string> {
|
||||
try {
|
||||
// if current os is windows, we don't need to install devchat
|
||||
if (os.platform() === "win32" && os.arch() === "x64") {
|
||||
// rewrite ._pth file in python directory
|
||||
const arch = os.arch();
|
||||
const targetPythonPath = os.arch() === "x64"? "python-3.11.6-embed-amd64" : "python-3.11.6-embed-arm64";
|
||||
const pythonTargetPath = path.join(UiUtilWrapper.extensionPath(), "tools", targetPythonPath);
|
||||
const pythonApp = path.join(pythonTargetPath, "python.exe");
|
||||
const pythonPathFile = path.join(pythonTargetPath, "python311._pth");
|
||||
const sitepackagesPath = path.join(UiUtilWrapper.extensionPath(), "tools", "site-packages");
|
||||
|
||||
// read content in pythonPathFile
|
||||
let content = fs.readFileSync(pythonPathFile, { encoding: 'utf-8' });
|
||||
// replace %PYTHONPATH% with sitepackagesPath
|
||||
content = content.replace(/%PYTHONPATH%/g, sitepackagesPath);
|
||||
// write content to pythonPathFile
|
||||
fs.writeFileSync(pythonPathFile, content);
|
||||
|
||||
// update DevChat.PythonForChat configration
|
||||
await UiUtilWrapper.updateConfiguration("DevChat", "PythonForChat", pythonApp);
|
||||
return pythonApp;
|
||||
} else {
|
||||
// if current os is not windows, we need to get default python path
|
||||
const pythonPath = getValidPythonCommand();
|
||||
if (pythonPath) {
|
||||
return pythonPath;
|
||||
}
|
||||
|
||||
logger.channel()?.info(`create env for python ...`);
|
||||
logger.channel()?.info(`try to create env by mamba ...`);
|
||||
let pythonCommand = await createEnvByMamba("devchat", "", "3.11.4");
|
||||
|
||||
if (!pythonCommand || pythonCommand === "") {
|
||||
logger.channel()?.info(`create env by mamba failed, try to create env by conda ...`);
|
||||
pythonCommand = await createEnvByConda("devchat", "", "3.11.4");
|
||||
}
|
||||
|
||||
if (!pythonCommand) {
|
||||
logger.channel()?.error('Create env failed');
|
||||
logger.channel()?.show();
|
||||
return '';
|
||||
}
|
||||
logger.channel()?.info(`Create env success: ${pythonCommand}`);
|
||||
|
||||
await UiUtilWrapper.updateConfiguration("DevChat", "PythonForChat", pythonCommand);
|
||||
return pythonCommand;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.channel()?.error(`${error}`);
|
||||
logger.channel()?.show();
|
||||
isDevChatInstalling = false;
|
||||
return '';
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
/*
|
||||
Install specific version of package. e.g. devchat
|
||||
*/
|
||||
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { logger } from '../logger';
|
||||
|
||||
// install specific version of package
|
||||
// pythonCommand -m install pkgName
|
||||
// if install success, return true
|
||||
// else return false
|
||||
export async function installPackage(pythonCommand: string, pkgName: string, otherSource: string | undefined) : Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let errorOut = '';
|
||||
|
||||
const cmd = pythonCommand;
|
||||
let args = ['-m', 'pip', 'install', pkgName, '--force-reinstall'];
|
||||
if (otherSource) {
|
||||
args.push("-i");
|
||||
args.push(otherSource);
|
||||
}
|
||||
const child = spawn(cmd, args);
|
||||
logger.channel()?.info(`Run command: ${cmd} ${args.join(' ')}`);
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
logger.channel()?.info(`${data}`);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
logger.channel()?.error(`${data}`);
|
||||
logger.channel()?.show();
|
||||
errorOut += data;
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
logger.channel()?.error(`exec error: ${error}`);
|
||||
logger.channel()?.show();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0 && errorOut !== "") {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function installRequirements(pythonCommand: string, requirementsFile: string, otherSource: string | undefined) : Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let errorOut = '';
|
||||
|
||||
const cmd = pythonCommand;
|
||||
let args = ['-m', 'pip', 'install', '-r', requirementsFile];
|
||||
if (otherSource) {
|
||||
args.push("-i");
|
||||
args.push(otherSource);
|
||||
}
|
||||
const child = spawn(cmd, args);
|
||||
logger.channel()?.info(`Run command: ${cmd} ${args.join(' ')}`);
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
logger.channel()?.info(`${data}`);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
logger.channel()?.error(`${data}`);
|
||||
logger.channel()?.show();
|
||||
errorOut += data;
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
logger.channel()?.error(`exec error: ${error}`);
|
||||
logger.channel()?.show();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0 && errorOut !== "") {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export function getPackageVersion(pythonPath: string, packageName: string): string | undefined {
|
||||
try {
|
||||
const stdout = execSync(`${pythonPath} -m pip show ${packageName}`).toString();
|
||||
const versionLine = stdout.split('\n').find(line => line.startsWith('Version'));
|
||||
return versionLine ? versionLine.split(': ')[1] : undefined;
|
||||
} catch (error) {
|
||||
console.error(`exec error: ${error}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
import { exec, spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { logger } from '../logger';
|
||||
const fs = require('fs');
|
||||
|
||||
// Check if the environment already exists
|
||||
export async function checkEnvExists(condaCommandPath: string, envName: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const condaCommand = path.resolve(condaCommandPath);
|
||||
const command = `${condaCommand} env list`;
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.channel()?.error(`Error checking environments`);
|
||||
logger.channel()?.show();
|
||||
reject(false);
|
||||
} else {
|
||||
const envs = stdout.split('\n').map(line => line.split(' ')[0]);
|
||||
resolve(envs.includes(envName));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Install env with specific python version
|
||||
// conda create -n {envName} python={pythonVersion} --yes
|
||||
// return: python in env path
|
||||
export async function installPython(condaCommandPath: string, envName: string, pythonVersion: string): Promise<string> {
|
||||
const envExists = await checkEnvExists(condaCommandPath, envName);
|
||||
|
||||
const condaCommand = path.resolve(condaCommandPath);
|
||||
const envPath = path.resolve(condaCommand, '..', '..', 'envs', envName);
|
||||
let pythonPath;
|
||||
let pythonPath2;
|
||||
if (os.platform() === 'win32') {
|
||||
pythonPath = path.join(envPath, 'Scripts', 'python.exe');
|
||||
pythonPath2 = path.join(envPath, 'python.exe');
|
||||
} else {
|
||||
pythonPath = path.join(envPath, 'bin', 'python');
|
||||
}
|
||||
|
||||
if (envExists) {
|
||||
if (fs.existsSync(pythonPath)) {
|
||||
return pythonPath;
|
||||
} else if (pythonPath2 && fs.existsSync(pythonPath2)) {
|
||||
return pythonPath2;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const cmd = condaCommand;
|
||||
const args = ['create', '-n', envName, `python=${pythonVersion}`, '--yes'];
|
||||
const child = spawn(cmd, args);
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
logger.channel()?.info(`${data}`);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
console.error(`stderr: ${data}`);
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
logger.channel()?.error(`Error installing python ${pythonVersion} in env ${envName}`);
|
||||
logger.channel()?.show();
|
||||
reject('');
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Command exited with code ${code}`));
|
||||
} else {
|
||||
if (fs.existsSync(pythonPath)) {
|
||||
resolve(pythonPath);
|
||||
} else if (pythonPath2 && fs.existsSync(pythonPath2)) {
|
||||
resolve(pythonPath2);
|
||||
} else {
|
||||
reject(new Error(`No Python found`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function canCreateSubdirectory(dirPath: string): boolean {
|
||||
try {
|
||||
const tempSubdirPath = path.join(dirPath, 'tempSubdirTest');
|
||||
fs.mkdirSync(tempSubdirPath);
|
||||
fs.rmdirSync(tempSubdirPath);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function installPythonMicromamba(mambaCommandPath: string, envName: string, pythonVersion: string): Promise<string> {
|
||||
// Set the installation directory for conda
|
||||
let userHome = process.platform === 'win32' ? fs.realpathSync(process.env.USERPROFILE || '') : process.env.HOME;
|
||||
if (os.platform() === 'win32' && /[^\x00-\xFF]/.test(userHome)) {
|
||||
if (fs.existsSync('C:/Program Files') && canCreateSubdirectory('C:/Program Files')) {
|
||||
userHome = 'C:/Program Files';
|
||||
} else if (fs.existsSync('D:/Program Files') && canCreateSubdirectory('D:/Program Files')) {
|
||||
userHome = 'D:/Program Files';
|
||||
} else if (fs.existsSync('E:/Program Files') && canCreateSubdirectory('E:/Program Files')) {
|
||||
userHome = 'E:/Program Files';
|
||||
}
|
||||
}
|
||||
const pathToMamba = `${userHome}/.chat/mamba`;
|
||||
|
||||
const envPath = path.resolve(pathToMamba, 'envs', envName);
|
||||
let pythonPath;
|
||||
let pythonPath2;
|
||||
if (os.platform() === 'win32') {
|
||||
pythonPath = path.join(envPath, 'Scripts', 'python.exe');
|
||||
pythonPath2 = path.join(envPath, 'python.exe');
|
||||
} else {
|
||||
pythonPath = path.join(envPath, 'bin', 'python');
|
||||
}
|
||||
|
||||
if (fs.existsSync(pythonPath)) {
|
||||
return pythonPath;
|
||||
} else if (pythonPath2 && fs.existsSync(pythonPath2)) {
|
||||
return pythonPath2;
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const cmd = mambaCommandPath;
|
||||
const args = ['create', '-n', envName, '-c', 'conda-forge', '-r', pathToMamba, `python=${pythonVersion}`, '--yes'];
|
||||
// output command and args in line
|
||||
// args to "create -n xx -c conda-forge ..."
|
||||
logger.channel()?.info(`cmd: ${cmd} ${args.join(' ')}`);
|
||||
const child = spawn(cmd, args);
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
logger.channel()?.info(`${data}`);
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
console.error(`stderr: ${data}`);
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
logger.channel()?.error(`Error installing python ${pythonVersion} in env ${envName}`);
|
||||
logger.channel()?.show();
|
||||
reject('');
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Command exited with code ${code}`));
|
||||
} else {
|
||||
if (fs.existsSync(pythonPath)) {
|
||||
resolve(pythonPath);
|
||||
} else if (pythonPath2 && fs.existsSync(pythonPath2)) {
|
||||
resolve(pythonPath2);
|
||||
} else {
|
||||
reject(new Error(`No Python found`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
# Why conda?
|
||||
Devchat-vscode support custom extension. Different extension may need different python version.
|
||||
|
||||
pyenv is also a python version manager, but it always download source of python, then build it to binary.
|
||||
|
||||
conda is a package manager, it can download binary of python, and install packages.
|
||||
|
||||
# Where to install?
|
||||
Install conda to $USER_PROFILE/.chat
|
||||
|
||||
Python will install inside $USER_PROFILE/.chat/conda.
|
||||
|
@ -1,19 +0,0 @@
|
||||
|
||||
let inMessages: object[] = [];
|
||||
let outMessages: object[] = [];
|
||||
|
||||
export function regInMessage(message: object) {
|
||||
inMessages.push(message);
|
||||
}
|
||||
|
||||
export function regOutMessage(message: object) {
|
||||
outMessages.push(message);
|
||||
}
|
||||
|
||||
export function getInMessages(): object[] {
|
||||
return inMessages;
|
||||
}
|
||||
|
||||
export function getOutMessages(): object[] {
|
||||
return outMessages;
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
|
||||
export interface UiUtil {
|
||||
languageId(uri: string): Promise<string>;
|
||||
workspaceFoldersFirstPath(): string | undefined;
|
||||
getConfiguration(key1: string, key2: string): string | undefined;
|
||||
updateConfiguration(key1: string, key2: string, value: string): Promise<void>;
|
||||
secretStorageGet(key: string): Promise<string | undefined>;
|
||||
writeFile(uri: string, content: string): Promise<void>;
|
||||
showInputBox(option: object): Promise<string | undefined>;
|
||||
storeSecret(key: string, value: string): Promise<void>;
|
||||
extensionPath(): string;
|
||||
runTerminal(terminalName:string, command: string): void;
|
||||
// current active file path
|
||||
activeFilePath(): string | undefined;
|
||||
// current selected range, return undefined if no selection
|
||||
selectRange(): [number, number] | undefined;
|
||||
// current selected text
|
||||
selectText(): string | undefined;
|
||||
showErrorMessage(message: string): void;
|
||||
getLSPBrigePort(): Promise<number | undefined>;
|
||||
}
|
||||
|
||||
|
||||
export class UiUtilWrapper {
|
||||
private static _uiUtil: UiUtil | undefined;
|
||||
public static init(uiUtil: UiUtil): void {
|
||||
this._uiUtil = uiUtil;
|
||||
}
|
||||
|
||||
public static async languageId(uri: string): Promise<string | undefined> {
|
||||
return await this._uiUtil?.languageId(uri);
|
||||
}
|
||||
public static workspaceFoldersFirstPath(): string | undefined {
|
||||
return this._uiUtil?.workspaceFoldersFirstPath();
|
||||
}
|
||||
public static getConfiguration(key1: string, key2: string): string | undefined {
|
||||
return this._uiUtil?.getConfiguration(key1, key2);
|
||||
}
|
||||
public static async updateConfiguration(key1: string, key2: string, value: string): Promise<void> {
|
||||
return await this._uiUtil?.updateConfiguration(key1, key2, value);
|
||||
}
|
||||
public static async secretStorageGet(key: string): Promise<string | undefined> {
|
||||
return await this._uiUtil?.secretStorageGet(key);
|
||||
}
|
||||
public static async writeFile(uri: string, content: string): Promise<void> {
|
||||
return await this._uiUtil?.writeFile(uri, content);
|
||||
}
|
||||
public static async showInputBox(option: object): Promise<string | undefined> {
|
||||
return await this._uiUtil?.showInputBox(option);
|
||||
}
|
||||
public static async storeSecret(key: string, value: string): Promise<void> {
|
||||
return await this._uiUtil?.storeSecret(key, value);
|
||||
}
|
||||
public static extensionPath(): string {
|
||||
return this._uiUtil?.extensionPath()!;
|
||||
}
|
||||
public static runTerminal(terminalName: string, command: string): void {
|
||||
this._uiUtil?.runTerminal(terminalName, command);
|
||||
}
|
||||
// current active file path
|
||||
public static activeFilePath(): string | undefined {
|
||||
return this._uiUtil?.activeFilePath();
|
||||
}
|
||||
// current selected range, return undefined if no selection
|
||||
public static selectRange(): [number, number] | undefined {
|
||||
return this._uiUtil?.selectRange();
|
||||
}
|
||||
// current selected text
|
||||
public static selectText(): string | undefined {
|
||||
return this._uiUtil?.selectText();
|
||||
}
|
||||
|
||||
public static showErrorMessage(message: string): void {
|
||||
this._uiUtil?.showErrorMessage(message);
|
||||
}
|
||||
|
||||
public static async getLSPBrigePort(): Promise<number | undefined> {
|
||||
return await this._uiUtil?.getLSPBrigePort();
|
||||
}
|
||||
}
|
||||
|
@ -1,130 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { ExtensionContextHolder } from './extensionContext';
|
||||
import { UiUtil } from './uiUtil';
|
||||
import { logger } from './logger';
|
||||
|
||||
|
||||
export class UiUtilVscode implements UiUtil {
|
||||
public async languageId(uri: string): Promise<string> {
|
||||
const document = await vscode.workspace.openTextDocument(uri);
|
||||
return document.languageId;
|
||||
}
|
||||
public workspaceFoldersFirstPath(): string | undefined {
|
||||
return vscode.workspace.workspaceFolders?.[0].uri.fsPath;
|
||||
}
|
||||
|
||||
public getConfiguration(key1: string, key2: string): string | undefined {
|
||||
return vscode.workspace.getConfiguration(key1).get(key2);
|
||||
}
|
||||
|
||||
public async updateConfiguration(key1: string, key2: string, value: string): Promise<void> {
|
||||
try {
|
||||
await vscode.workspace.getConfiguration(key1).update(key2, value, vscode.ConfigurationTarget.Global);
|
||||
await vscode.workspace.getConfiguration(key1).update(key2, value, vscode.ConfigurationTarget.Workspace);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
public async secretStorageGet(key: string): Promise<string | undefined> {
|
||||
try {
|
||||
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
|
||||
let openaiApiKey = await secretStorage.get(key);
|
||||
return openaiApiKey;
|
||||
} catch (error) {
|
||||
logger.channel()?.error(`secretStorageGet error: ${error}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
public async writeFile(uri: string, content: string): Promise<void> {
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(uri), Buffer.from(content));
|
||||
}
|
||||
public async showInputBox(option: object): Promise<string | undefined> {
|
||||
return vscode.window.showInputBox(option);
|
||||
}
|
||||
public async storeSecret(key: string, value: string): Promise<void> {
|
||||
const secretStorage: vscode.SecretStorage = ExtensionContextHolder.context!.secrets;
|
||||
await secretStorage.store(key, value);
|
||||
}
|
||||
public extensionPath(): string {
|
||||
return ExtensionContextHolder.context!.extensionUri.fsPath;
|
||||
}
|
||||
public runTerminal(terminalName: string, command: string): void {
|
||||
const terminals = vscode.window.terminals;
|
||||
for (const terminal of terminals) {
|
||||
if (terminal.name === terminalName) {
|
||||
terminal.dispose();
|
||||
}
|
||||
}
|
||||
const terminal = vscode.window.createTerminal(terminalName);
|
||||
terminal.sendText(command);
|
||||
terminal.show();
|
||||
}
|
||||
|
||||
// current active file path
|
||||
public activeFilePath(): string | undefined {
|
||||
const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined);
|
||||
|
||||
if (validVisibleTextEditors.length > 1) {
|
||||
vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const editor = validVisibleTextEditors[0];
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return editor.document.fileName;
|
||||
}
|
||||
// current selected range, return undefined if no selection
|
||||
public selectRange(): [number, number] | undefined {
|
||||
const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined);
|
||||
|
||||
if (validVisibleTextEditors.length > 1) {
|
||||
vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const editor = validVisibleTextEditors[0];
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (editor.selection.isEmpty) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [editor.selection.start.character, editor.selection.end.character];
|
||||
}
|
||||
// current selected text
|
||||
public selectText(): string | undefined {
|
||||
const validVisibleTextEditors = vscode.window.visibleTextEditors.filter(editor => editor.viewColumn !== undefined);
|
||||
|
||||
if (validVisibleTextEditors.length > 1) {
|
||||
vscode.window.showErrorMessage(`There are more then one visible text editors. Please close all but one and try again.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const editor = validVisibleTextEditors[0];
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (editor.selection.isEmpty) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return editor.document.getText(editor.selection);
|
||||
}
|
||||
|
||||
public showErrorMessage(message: string): void {
|
||||
vscode.window.showErrorMessage(message);
|
||||
}
|
||||
|
||||
public async getLSPBrigePort(): Promise<number | undefined> {
|
||||
const port = await vscode.commands.executeCommand('LangBrige.getAddress') as number | undefined;;
|
||||
return port;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ES2020",
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"es6",
|
||||
"dom"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": false,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"test"
|
||||
]
|
||||
}
|
9
tsconfig.test.json
Normal file
9
tsconfig.test.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "out-test",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user