Add client: VSCode. (#21)

add-more-languages
Zhiming Ma 2023-03-28 15:53:57 +08:00 committed by GitHub
parent c990ba843f
commit e992a0144b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2663 additions and 0 deletions

View File

@ -0,0 +1,18 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/naming-convention": "warn",
"@typescript-eslint/semi": "warn",
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": ["out", "dist", "**/*.d.ts"]
}

5
tabby/clients/vscode/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
out
dist
node_modules
.vscode-test/
*.vsix

View File

@ -0,0 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"]
}

View File

@ -0,0 +1,36 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension without Local Extentions",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-extensions"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "tasks: watch-tests"
}
]
}

View File

@ -0,0 +1,27 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false, // set this to true to hide the "out" folder with the compiled JS files
"dist": false // set this to true to hide the "dist" folder with the compiled JS files
},
"search.exclude": {
"out": true, // set this to false to include "out" folder in search results
"dist": true // set this to false to include "dist" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
"terminal.integrated.profiles.windows": {
"PowerShell": {
"source": "PowerShell",
"icon": "terminal-powershell"
},
"Command Prompt": {
"path": ["${env:windir}\\Sysnative\\cmd.exe", "${env:windir}\\System32\\cmd.exe"],
"args": [],
"icon": "terminal-cmd"
},
"Git Bash": {
"source": "Git Bash"
}
}
}

37
tabby/clients/vscode/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,37 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$ts-webpack-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "watch-tests",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never",
"group": "watchers"
},
"group": "build"
},
{
"label": "tasks: watch-tests",
"dependsOn": ["npm: watch", "npm: watch-tests"],
"problemMatcher": []
}
]
}

View File

@ -0,0 +1,12 @@
.vscode/**
.vscode-test/**
out/**
node_modules/**
src/**
.gitignore
.yarnrc
webpack.config.js
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts

View File

@ -0,0 +1 @@
# TODO

View File

@ -0,0 +1,81 @@
{
"name": "vscode-tabby",
"displayName": "Tabby",
"description": "Get completions from Tabby server",
"version": "1.0.0",
"keywords": [
"code-suggestion",
"copilot",
"code-inference",
"tabby"
],
"engines": {
"vscode": "^1.70.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onStartupFinished"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "tabby.toggleEnabled",
"title": "Tabby: Toggle Code Suggestion On/Off"
},
{
"command": "tabby.setServerUrl",
"title": "Tabby: Set URL of Tabby Server"
}
],
"configuration": {
"title": "Tabby",
"properties": {
"tabby.enabled": {
"type": "boolean",
"default": true,
"description": "Enable Tabby code suggestion or not."
},
"tabby.serverUrl": {
"type": "string",
"default": "http://127.0.0.1:5000",
"markdownDescription": "Specifies the url of [Tabby Server](https://github.com/TabbyML/tabby)."
}
}
}
},
"scripts": {
"vscode:prepublish": "yarn package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "yarn compile-tests && yarn compile && yarn lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js",
"vscode:package": "vsce package",
"vscode:publish": "vsce publish"
},
"devDependencies": {
"@types/glob": "^7.2.0",
"@types/mocha": "^9.1.1",
"@types/node": "16.x",
"@types/vscode": "^1.70.0",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
"@vscode/test-electron": "^2.1.5",
"eslint": "^8.20.0",
"glob": "^8.0.3",
"mocha": "^10.0.0",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"dependencies": {
"axios": "^1.3.4"
}
}

View File

@ -0,0 +1,50 @@
import { ConfigurationTarget, workspace, window, commands } from "vscode";
import { EventHandler } from "./EventHandler";
const target = ConfigurationTarget.Global;
type Command = {
command: string;
callback: (...args: any[]) => any;
thisArg?: any;
};
const toogleEnabled: Command = {
command: "tabby.toggleEnabled",
callback: () => {
const configuration = workspace.getConfiguration("tabby");
const enabled = configuration.get("enabled", true);
console.debug(`Toggle Enabled: ${enabled} -> ${!enabled}.`);
configuration.update("enabled", !enabled, target, false);
},
};
const setServerUrl: Command = {
command: "tabby.setServerUrl",
callback: () => {
const configuration = workspace.getConfiguration("tabby");
window
.showInputBox({
prompt: "Enter the URL of your Tabby Server",
value: configuration.get("serverUrl", ""),
})
.then((url) => {
if (url) {
console.debug("Set Tabby Server URL: ", url);
configuration.update("serverUrl", url, target, false);
}
});
},
};
const eventHandler = new EventHandler();
const emitEvent: Command = {
command: "tabby.emitEvent",
callback: (event) => {
eventHandler.handle(event);
},
};
export const tabbyCommands = [toogleEnabled, setServerUrl, emitEvent].map((command) =>
commands.registerCommand(command.command, command.callback, command.thisArg)
);

View File

@ -0,0 +1,43 @@
import { workspace } from "vscode";
import axios from "axios";
export enum EventType {
InlineCompletionDisplayed,
InlineCompletionAccepted,
}
export interface Event {
type: EventType,
id?: string,
index?: number,
}
export class EventHandler {
private tabbyServerUrl: string = "";
constructor() {
this.updateConfiguration();
workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("tabby")) {
this.updateConfiguration();
}
});
}
handle(event: Event) {
console.debug("Event: ", event);
switch (event.type) {
case EventType.InlineCompletionDisplayed:
axios.post(`${this.tabbyServerUrl}/v1/completions/${event.id}/choices/${event.index}/view`);
break;
case EventType.InlineCompletionAccepted:
axios.post(`${this.tabbyServerUrl}/v1/completions/${event.id}/choices/${event.index}/select`);
break;
}
}
private updateConfiguration() {
const configuration = workspace.getConfiguration("tabby");
this.tabbyServerUrl = configuration.get("serverUrl", "http://127.0.0.1:5000");
}
}

View File

@ -0,0 +1,134 @@
import {
CancellationToken,
InlineCompletionContext,
InlineCompletionItem,
InlineCompletionItemProvider,
InlineCompletionList,
Position,
ProviderResult,
Range,
TextDocument,
workspace,
} from "vscode";
import axios, { AxiosResponse } from "axios";
import { EventType } from "./EventHandler";
export class TabbyCompletionProvider implements InlineCompletionItemProvider {
private uuid = Date.now();
private latestTimestamp: number = 0;
// User Settings
private enabled: boolean = true;
private tabbyServerUrl: string = "";
constructor() {
this.updateConfiguration();
workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration("tabby")) {
this.updateConfiguration();
}
});
}
//@ts-ignore because ASYNC and PROMISE
//prettier-ignore
public async provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<InlineCompletionItem[] | InlineCompletionList> {
const emptyResponse = Promise.resolve([] as InlineCompletionItem[]);
if (!this.enabled) {
console.debug("Extension not enabled, skipping.");
return emptyResponse;
}
const prompt = this.getPrompt(document, position);
if (this.isNil(prompt)) {
console.debug("Prompt is empty, skipping");
return emptyResponse;
}
const currentTimestamp = Date.now();
this.latestTimestamp = currentTimestamp;
const suggestionDelay = 150;
await this.sleep(suggestionDelay);
if (currentTimestamp < this.latestTimestamp) {
return emptyResponse;
}
console.debug(
"Requesting: ",
{
uuid: this.uuid,
timestamp: currentTimestamp,
prompt
}
);
// Prompt is already nil-checked
const response = await this.getCompletions(prompt as String);
const hasSuffixParen = this.hasSuffixParen(document, position);
const replaceRange = hasSuffixParen
? new Range(
position.line,
position.character,
position.line,
position.character + 1
)
: new Range(position, position);
const completions = this.toInlineCompletions(response.data, replaceRange);
console.debug("Result completions: ", completions);
return Promise.resolve(completions);
}
private updateConfiguration() {
const configuration = workspace.getConfiguration("tabby");
this.enabled = configuration.get("enabled", true);
this.tabbyServerUrl = configuration.get("serverUrl", "http://127.0.0.1:5000");
}
private getPrompt(document: TextDocument, position: Position): String | undefined {
const maxLines = 20;
const firstLine = Math.max(position.line - maxLines, 0);
return document.getText(new Range(firstLine, 0, position.line, position.character));
}
private isNil(value: String | undefined | null): boolean {
return value === undefined || value === null || value.length === 0;
}
private sleep(milliseconds: number) {
return new Promise((r) => setTimeout(r, milliseconds));
}
private toInlineCompletions(value: any, range: Range): InlineCompletionItem[] {
return (
value.choices?.map(
(choice: any) =>
new InlineCompletionItem(choice.text, range, {
title: "Tabby: Emit Event",
command: "tabby.emitEvent",
arguments: [
{
type: EventType.InlineCompletionAccepted,
id: value.id,
index: choice.index,
},
],
})
) || []
);
}
private getCompletions(prompt: String): Promise<AxiosResponse<any, any>> {
return axios.post(`${this.tabbyServerUrl}/v1/completions`, {
prompt,
});
}
private hasSuffixParen(document: TextDocument, position: Position) {
const suffix = document.getText(
new Range(position.line, position.character, position.line, position.character + 1)
);
return ")]}".indexOf(suffix) > -1;
}
}

View File

@ -0,0 +1,23 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import { ExtensionContext, languages } from "vscode";
import { tabbyCommands } from "./Commands";
import { TabbyCompletionProvider } from "./TabbyCompletionProvider";
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: ExtensionContext) {
console.debug("Activating Tabby extension", new Date());
context.subscriptions.push(
languages.registerInlineCompletionItemProvider(
{ pattern: "**" },
new TabbyCompletionProvider()
),
...tabbyCommands
);
}
// this method is called when your extension is deactivated
export function deactivate() {
console.debug("Deactivating Tabby extension", new Date());
}

View File

@ -0,0 +1,23 @@
import * as path from "path";
import { runTests } from "@vscode/test-electron";
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, "../../");
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, "./suite/index");
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error("Failed to run tests");
process.exit(1);
}
}
main();

View File

@ -0,0 +1,15 @@
import * as assert from "assert";
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from "vscode";
// import * as myExtension from '../../extension';
suite("Extension Test Suite", () => {
vscode.window.showInformationMessage("Start all tests.");
test("Sample test", () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

View File

@ -0,0 +1,38 @@
import * as path from "path";
import * as Mocha from "mocha";
import * as glob from "glob";
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: "tdd",
color: true,
});
const testsRoot = path.resolve(__dirname, "..");
return new Promise((c, e) => {
glob("**/**.test.js", { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"lib": ["ES2020"],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */,
/* Additional Checks */
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"noUnusedParameters": true /* Report errors on unused parameters. */
}
}

View File

@ -0,0 +1,48 @@
//@ts-check
'use strict';
const path = require('path');
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/** @type WebpackConfig */
const extensionConfig = {
target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
},
externals: {
vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
// modules added here also need to be added in the .vscodeignore file
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}
]
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
};
module.exports = [ extensionConfig ];

File diff suppressed because it is too large Load Diff