Inital commit/ Base to the extension
parent
5c0dd9a556
commit
02ebd7b198
|
@ -1,132 +1,3 @@
|
|||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'test/**/*.test.js',
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"ms-vscode.extension-test-runner"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
// A launch configuration that launches the extension 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",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.vscode/**
|
||||
.vscode-test/**
|
||||
test/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/jsconfig.json
|
||||
**/*.map
|
||||
**/eslint.config.mjs
|
||||
**/.vscode-test.*
|
|
@ -0,0 +1,9 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to the "93m1n1gpt" extension will be documented in this file.
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Initial release
|
66
README.md
66
README.md
|
@ -1,3 +1,65 @@
|
|||
# 93m1n1gpt
|
||||
# 93m1n1gpt README
|
||||
|
||||
A local llm pair programming ai assistant.
|
||||
This is the README for your extension "93m1n1gpt". After writing up a brief description, we recommend including the following sections.
|
||||
|
||||
## Features
|
||||
|
||||
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
|
||||
|
||||
For example if there is an image subfolder under your extension project workspace:
|
||||
|
||||
\!\[feature X\]\(images/feature-x.png\)
|
||||
|
||||
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
|
||||
|
||||
## Requirements
|
||||
|
||||
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
|
||||
|
||||
## Extension Settings
|
||||
|
||||
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
|
||||
|
||||
For example:
|
||||
|
||||
This extension contributes the following settings:
|
||||
|
||||
* `myExtension.enable`: Enable/disable this extension.
|
||||
* `myExtension.thing`: Set to `blah` to do something.
|
||||
|
||||
## Known Issues
|
||||
|
||||
Calling out known issues can help limit users opening duplicate issues against your extension.
|
||||
|
||||
## Release Notes
|
||||
|
||||
Users appreciate release notes as you update your extension.
|
||||
|
||||
### 1.0.0
|
||||
|
||||
Initial release of ...
|
||||
|
||||
### 1.0.1
|
||||
|
||||
Fixed issue #.
|
||||
|
||||
### 1.1.0
|
||||
|
||||
Added features X, Y, and Z.
|
||||
|
||||
---
|
||||
|
||||
## Working with Markdown
|
||||
|
||||
You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
|
||||
|
||||
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux)
|
||||
* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux)
|
||||
* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets
|
||||
|
||||
## For more information
|
||||
|
||||
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
|
||||
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
|
||||
|
||||
**Enjoy!**
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import globals from "globals";
|
||||
|
||||
export default [{
|
||||
files: ["**/*.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.commonjs,
|
||||
...globals.node,
|
||||
...globals.mocha,
|
||||
},
|
||||
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
|
||||
rules: {
|
||||
"no-const-assign": "warn",
|
||||
"no-this-before-super": "warn",
|
||||
"no-undef": "warn",
|
||||
"no-unreachable": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"constructor-super": "warn",
|
||||
"valid-typeof": "warn",
|
||||
},
|
||||
}];
|
|
@ -0,0 +1,50 @@
|
|||
const vscode = require("vscode");
|
||||
const { AISidebarProvider } = require('./src/utils/AISidebarProvider');
|
||||
const LLMSelectorPanel = require('./src/settings/panels/llmSelector');
|
||||
const SecureSettingsManager = require('./src/settings/secureSettingsManager');
|
||||
const { RepoTracker } = require('./src/utils/RepoTracker');
|
||||
|
||||
async function activate(context) {
|
||||
// Initialize secure settings manager first
|
||||
const settingsManager = new SecureSettingsManager(context);
|
||||
await settingsManager.initialize();
|
||||
|
||||
// Initialize RepoTracker
|
||||
const repoTracker = new RepoTracker();
|
||||
await repoTracker.initialize();
|
||||
|
||||
// Set up git watcher
|
||||
const gitWatcher = await repoTracker.watchGitChanges();
|
||||
context.subscriptions.push(gitWatcher);
|
||||
|
||||
// Initialize providers with secure settings manager and repoTracker
|
||||
const provider = new AISidebarProvider(context.extensionUri, settingsManager, repoTracker);
|
||||
|
||||
// Register the sidebar provider
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider('C4B3Rstudios.93m1n1gpt', provider)
|
||||
);
|
||||
|
||||
// Register the LLM selector command
|
||||
let llmSelectorCommand = vscode.commands.registerCommand(
|
||||
'C4B3Rstudios.93m1n1gpt.showLLMSelector',
|
||||
() => {
|
||||
const llmSelector = new LLMSelectorPanel(context.extensionUri, settingsManager);
|
||||
llmSelector.createOrShow(vscode.ViewColumn.One);
|
||||
}
|
||||
);
|
||||
|
||||
// Register the main command
|
||||
let mainCommand = vscode.commands.registerCommand('C4B3Rstudios.93m1n1gpt', () => {
|
||||
vscode.window.showInformationMessage('93m1n1gpt is now active!');
|
||||
});
|
||||
|
||||
context.subscriptions.push(llmSelectorCommand, mainCommand);
|
||||
}
|
||||
|
||||
function deactivate() { }
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "ES2022",
|
||||
"checkJs": true, /* Typecheck .js files. */
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"name": "93m1n1gpt",
|
||||
"displayName": "93m1n1gpt",
|
||||
"description": "Agent flow BYOLLM pair programming assistint ",
|
||||
"publisher": "C4B3Rstudios",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.97.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [],
|
||||
"main": "./extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "C4B3Rstudios.93m1n1gpt",
|
||||
"title": "Open 93m1n1GPT",
|
||||
"category": "93m1n1GPT"
|
||||
},
|
||||
{
|
||||
"command": "C4B3Rstudios.93m1n1gpt.showLLMSelector",
|
||||
"title": "93m1n1GPT: Show LLM Selector"
|
||||
}
|
||||
],
|
||||
"viewsContainers": {
|
||||
"activitybar": [
|
||||
{
|
||||
"id": "93m1n1gpt-sidebar",
|
||||
"title": "93m1n1GPT",
|
||||
"icon": "resources/icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"views": {
|
||||
"93m1n1gpt-sidebar": [
|
||||
{
|
||||
"type": "webview",
|
||||
"id": "C4B3Rstudios.93m1n1gpt",
|
||||
"name": "93m1n1GPT Chat",
|
||||
"icon": "resources/icon.svg"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
"title": "93m1n1GPT",
|
||||
"properties": {
|
||||
"93m1n1gpt.selectedModel": {
|
||||
"type": "string",
|
||||
"default": "llama3.2:3b",
|
||||
"description": "Selected LLM model to use"
|
||||
},
|
||||
"93m1n1gpt.apiUrl": {
|
||||
"type": "string",
|
||||
"default": "http://localhost:11434",
|
||||
"description": "Ollama API URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"pretest": "npm run lint",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "20.x",
|
||||
"@types/vscode": "^1.97.0",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"eslint": "^9.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.3.29",
|
||||
"@langchain/core": "^0.3.39",
|
||||
"@langchain/ollama": "^0.2.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"marked": "^15.0.7"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 12V22H4V2H20V12ZM18 14H6V20H18V14ZM18 4H6V12H18V4Z" fill="#424242"/>
|
||||
<path d="M8 6H16V8H8V6Z" fill="#424242"/>
|
||||
<path d="M8 16H16V18H8V16Z" fill="#424242"/>
|
||||
</svg>
|
After Width: | Height: | Size: 284 B |
|
@ -0,0 +1,63 @@
|
|||
// ChatService.js
|
||||
const vscode = require("vscode")
|
||||
const { ChatOllama } = require("@langchain/ollama");
|
||||
const { PromptTemplate } = require("@langchain/core/prompts");
|
||||
|
||||
class ChatService {
|
||||
constructor(settingsManager) {
|
||||
this._settingsManager = settingsManager;
|
||||
}
|
||||
|
||||
async handleChatMessage(webviewView, userMessage, messageHistory, markdownRenderer, repoTracker) {
|
||||
messageHistory.push({ role: "user", content: userMessage });
|
||||
|
||||
const config = this._getConfig();
|
||||
const contextInfo = await this._getContextInfo(repoTracker);
|
||||
const chat = this._createChatInstance(config);
|
||||
|
||||
await this._streamResponse(chat, messageHistory, webviewView, markdownRenderer);
|
||||
}
|
||||
|
||||
_getConfig() {
|
||||
const config = vscode.workspace.getConfiguration('93m1n1gpt');
|
||||
return {
|
||||
model: config.get('selectedModel') || "llama2:13b",
|
||||
apiUrl: config.get('apiUrl') || "http://localhost:11434"
|
||||
};
|
||||
}
|
||||
|
||||
async _getContextInfo(repoTracker) {
|
||||
const gitStatus = await repoTracker.getGitStatus();
|
||||
return gitStatus ?
|
||||
`\nCurrent git branch: ${gitStatus.branch}\nLast commit: ${gitStatus.lastCommit}` : '';
|
||||
}
|
||||
|
||||
_createChatInstance(config) {
|
||||
return new ChatOllama({
|
||||
model: config.model,
|
||||
baseUrl: config.apiUrl,
|
||||
temperature: 0.6,
|
||||
topP: 0.9,
|
||||
topK: 40,
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
|
||||
async _streamResponse(chat, messageHistory, webviewView, markdownRenderer) {
|
||||
let currentStreamedText = "";
|
||||
const stream = await chat.stream([...messageHistory]);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
currentStreamedText += chunk.content;
|
||||
const formattedMarkdown = markdownRenderer.render(currentStreamedText);
|
||||
webviewView.webview.postMessage({
|
||||
command: "updateBotMessage",
|
||||
html: formattedMarkdown
|
||||
});
|
||||
}
|
||||
|
||||
messageHistory.push({ role: "assistant", content: currentStreamedText });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ChatService };
|
|
@ -0,0 +1,58 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ollama LLM Selection</title>
|
||||
<!-- Include any necessary CSS frameworks or stylesheets here -->
|
||||
</head>
|
||||
<body>
|
||||
<h1>Select an LLM</h1>
|
||||
<p>Choose an LLM from the list below:</p>
|
||||
<select id="llmSelector">
|
||||
<!-- Options will be dynamically populated with available LLMs -->
|
||||
</select>
|
||||
<button id="selectButton">Select</button>
|
||||
|
||||
<script>
|
||||
// Function to fetch available LLMs from Ollama
|
||||
async function fetchAvailableLLMs() {
|
||||
try {
|
||||
const response = await fetch('https://api.ollama.ai/tags');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch available LLMs');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.tags;
|
||||
} catch (error) {
|
||||
console.error('Error fetching available LLMs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Function to populate the LLM selector with options
|
||||
async function populateLLMSelector() {
|
||||
const llms = await fetchAvailableLLMs();
|
||||
const llmSelector = document.getElementById('llmSelector');
|
||||
|
||||
llms.forEach(llm => {
|
||||
const option = document.createElement('option');
|
||||
option.value = llm;
|
||||
option.textContent = llm;
|
||||
llmSelector.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Event listener for the "Select" button
|
||||
document.getElementById('selectButton').addEventListener('click', () => {
|
||||
const selectedLLM = document.getElementById('llmSelector').value;
|
||||
// Here, you can perform actions based on the selected LLM, such as sending it to the extension.
|
||||
console.log('Selected LLM:', selectedLLM);
|
||||
// You can also send a message to the extension to set up the selected LLM.
|
||||
});
|
||||
|
||||
// Call the function to populate the LLM selector
|
||||
populateLLMSelector();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,348 @@
|
|||
const vscode = require('vscode');
|
||||
|
||||
class LLMSelectorPanel {
|
||||
constructor(extensionUri, settingsManager) {
|
||||
this.extensionUri = extensionUri;
|
||||
this.panel = null;
|
||||
this.settingsManager = settingsManager;
|
||||
}
|
||||
|
||||
async createOrShow(viewColumn) {
|
||||
if (this.panel) {
|
||||
this.panel.reveal(viewColumn);
|
||||
} else {
|
||||
this.panel = vscode.window.createWebviewPanel(
|
||||
'C4B3Rstudios.93m1n1gpt.llmSelector',
|
||||
'93m1n1GPT LLM Selector',
|
||||
viewColumn,
|
||||
{
|
||||
enableScripts: true,
|
||||
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'media')]
|
||||
}
|
||||
);
|
||||
|
||||
this.panel.webview.html = await this.getWebviewContent(this.panel.webview);
|
||||
|
||||
this.panel.webview.onDidReceiveMessage(
|
||||
async message => {
|
||||
switch (message.command) {
|
||||
case 'selectLLM':
|
||||
await vscode.workspace.getConfiguration('93m1n1gpt').update(
|
||||
'selectedModel',
|
||||
message.model,
|
||||
vscode.ConfigurationTarget.Global
|
||||
);
|
||||
vscode.window.showInformationMessage(`Selected LLM: ${message.model}`);
|
||||
this.panel.dispose();
|
||||
break;
|
||||
case 'updateApiUrl':
|
||||
await this.settingsManager.setSecure('apiUrl', message.url);
|
||||
vscode.window.showInformationMessage('API URL updated securely');
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.panel.onDidDispose(() => {
|
||||
this.panel = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getWebviewContent(webview) {
|
||||
try {
|
||||
const apiUrl = await this.settingsManager.getSecure('apiUrl') || 'http://localhost:11434';
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>93m1n1GPT LLM Selection</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: var(--vscode-font-family);
|
||||
color: var(--vscode-foreground);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin: 0 0 20px 0;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--vscode-foreground);
|
||||
margin: 20px 0 15px 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vscode-foreground);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input[type="text"], select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
select {
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: var(--vscode-inputValidation-errorBackground);
|
||||
border: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||
color: var(--vscode-inputValidation-errorForeground);
|
||||
}
|
||||
|
||||
.status.info {
|
||||
background: var(--vscode-infoBackground);
|
||||
border: 1px solid var(--vscode-infoBorder);
|
||||
color: var(--vscode-infoForeground);
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.model-count {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 2px solid var(--vscode-input-background);
|
||||
border-top: 2px solid var(--vscode-focusBorder);
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 1s linear infinite;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>93m1n1GPT Configuration</h1>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="apiUrl">Ollama API URL</label>
|
||||
<input type="text"
|
||||
id="apiUrl"
|
||||
value="${apiUrl}"
|
||||
placeholder="Enter Ollama API URL"
|
||||
/>
|
||||
<button id="saveApiUrl">Save API URL</button>
|
||||
</div>
|
||||
|
||||
<h2>LLM Model Selection</h2>
|
||||
<div class="input-group">
|
||||
<label for="llmSelector">Available Models</label>
|
||||
<select id="llmSelector">
|
||||
<option value="">Loading available models...</option>
|
||||
</select>
|
||||
<div class="model-count" id="modelCount"></div>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status" style="display: none;"></div>
|
||||
|
||||
<div class="button-container">
|
||||
<button id="selectButton" disabled>Select Model</button>
|
||||
<button id="refreshModels">
|
||||
<span id="refreshSpinner" class="spinner" style="display: none;"></span>
|
||||
Refresh Models
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
const llmSelector = document.getElementById('llmSelector');
|
||||
const selectButton = document.getElementById('selectButton');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const apiUrlInput = document.getElementById('apiUrl');
|
||||
const saveApiUrlButton = document.getElementById('saveApiUrl');
|
||||
const refreshButton = document.getElementById('refreshModels');
|
||||
const refreshSpinner = document.getElementById('refreshSpinner');
|
||||
const modelCountDiv = document.getElementById('modelCount');
|
||||
|
||||
async function fetchAvailableLLMs() {
|
||||
try {
|
||||
refreshSpinner.style.display = 'inline-block';
|
||||
const apiUrl = apiUrlInput.value;
|
||||
const response = await fetch(apiUrl + '/api/tags');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch available LLMs');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.models || [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching available LLMs:', error);
|
||||
showError('Failed to fetch models. Please ensure Ollama is running.');
|
||||
return [];
|
||||
} finally {
|
||||
refreshSpinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
statusDiv.style.display = 'block';
|
||||
statusDiv.className = 'status error';
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
function showStatus(message) {
|
||||
statusDiv.style.display = 'block';
|
||||
statusDiv.className = 'status info';
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
async function populateLLMSelector() {
|
||||
showStatus('Fetching available models...');
|
||||
const llms = await fetchAvailableLLMs();
|
||||
|
||||
llmSelector.innerHTML = '';
|
||||
if (llms.length === 0) {
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = 'No models found';
|
||||
llmSelector.appendChild(option);
|
||||
selectButton.disabled = true;
|
||||
modelCountDiv.textContent = 'No models available';
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.value = '';
|
||||
defaultOption.textContent = 'Select a model';
|
||||
llmSelector.appendChild(defaultOption);
|
||||
|
||||
llms.forEach(llm => {
|
||||
const option = document.createElement('option');
|
||||
option.value = llm.name;
|
||||
option.textContent = llm.name;
|
||||
llmSelector.appendChild(option);
|
||||
});
|
||||
|
||||
modelCountDiv.textContent = \`\${llms.length} model\${llms.length === 1 ? '' : 's'} available\`;
|
||||
selectButton.disabled = true;
|
||||
statusDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
llmSelector.addEventListener('change', () => {
|
||||
selectButton.disabled = !llmSelector.value;
|
||||
});
|
||||
|
||||
selectButton.addEventListener('click', () => {
|
||||
const selectedLLM = llmSelector.value;
|
||||
if (selectedLLM) {
|
||||
vscode.postMessage({
|
||||
command: 'selectLLM',
|
||||
model: selectedLLM
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
saveApiUrlButton.addEventListener('click', async () => {
|
||||
showStatus('Updating API URL...');
|
||||
await vscode.postMessage({
|
||||
command: 'updateApiUrl',
|
||||
url: apiUrlInput.value
|
||||
});
|
||||
populateLLMSelector();
|
||||
});
|
||||
|
||||
refreshButton.addEventListener('click', () => {
|
||||
populateLLMSelector();
|
||||
});
|
||||
|
||||
// Initialize the selector
|
||||
populateLLMSelector();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
} catch (error) {
|
||||
console.error('Error generating webview content:', error);
|
||||
return `<html><body>Error loading content: ${error.message}</body></html>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LLMSelectorPanel;
|
|
@ -0,0 +1,74 @@
|
|||
const vscode = require('vscode');
|
||||
|
||||
class SecureSettingsManager {
|
||||
constructor(context) {
|
||||
this.secretStorage = context.secrets;
|
||||
this.CONFIG_PREFIX = '93m1n1gpt.';
|
||||
this.SECURE_KEYS = ['apiUrl']; // List of keys that should be stored securely
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Migrate any existing settings to secure storage
|
||||
const config = vscode.workspace.getConfiguration('93m1n1gpt');
|
||||
for (const key of this.SECURE_KEYS) {
|
||||
const existingValue = config.get(key);
|
||||
if (existingValue) {
|
||||
await this.setSecure(key, existingValue);
|
||||
// Clear the value from regular configuration
|
||||
await config.update(key, undefined, vscode.ConfigurationTarget.Global);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getSecure(key) {
|
||||
try {
|
||||
const value = await this.secretStorage.get(this.CONFIG_PREFIX + key);
|
||||
return value || null;
|
||||
} catch (error) {
|
||||
console.error(`Error retrieving secure setting ${key}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setSecure(key, value) {
|
||||
try {
|
||||
await this.secretStorage.store(this.CONFIG_PREFIX + key, value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error storing secure setting ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSecure(key) {
|
||||
try {
|
||||
await this.secretStorage.delete(this.CONFIG_PREFIX + key);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error deleting secure setting ${key}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get all settings (both secure and non-secure)
|
||||
async getAllSettings() {
|
||||
const config = vscode.workspace.getConfiguration('93m1n1gpt');
|
||||
const settings = {};
|
||||
|
||||
// Get non-secure settings
|
||||
for (const key of Object.keys(config)) {
|
||||
if (!this.SECURE_KEYS.includes(key)) {
|
||||
settings[key] = config.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Get secure settings
|
||||
for (const key of this.SECURE_KEYS) {
|
||||
settings[key] = await this.getSecure(key);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SecureSettingsManager;
|
|
@ -0,0 +1,60 @@
|
|||
// AISidebarProvider.js
|
||||
const { MarkdownRenderer } = require("../utils/MarkdownRender");
|
||||
const { ChatService } = require("../handlers/ChatService");
|
||||
const { FileSystemManager } = require("./FileSystemManager");
|
||||
const { GitManager } = require("./GitManager");
|
||||
const { UIManager } = require("./UIManager");
|
||||
|
||||
class AISidebarProvider {
|
||||
constructor(extensionUri, settingsManager, repoTracker) {
|
||||
this._extensionUri = extensionUri;
|
||||
this._settingsManager = settingsManager;
|
||||
this.repoTracker = repoTracker;
|
||||
this.messageHistory = [];
|
||||
this._disposables = [];
|
||||
|
||||
// Initialize services
|
||||
this.markdownRenderer = new MarkdownRenderer();
|
||||
this.chatService = new ChatService(settingsManager);
|
||||
this.fileManager = new FileSystemManager(repoTracker);
|
||||
this.gitManager = new GitManager(repoTracker);
|
||||
this.uiManager = new UIManager(this._extensionUri);
|
||||
}
|
||||
|
||||
async resolveWebviewView(webviewView, context, _token) {
|
||||
this._view = webviewView;
|
||||
this.uiManager.initializeWebview(webviewView, this._extensionUri);
|
||||
|
||||
await this._setupServices(webviewView);
|
||||
this._setupMessageHandlers(webviewView);
|
||||
}
|
||||
|
||||
async _setupServices(webviewView) {
|
||||
await this.fileManager.setupFileWatcher(webviewView, this._disposables);
|
||||
await this.gitManager.setupGitWatcher(webviewView, this._disposables);
|
||||
}
|
||||
|
||||
_setupMessageHandlers(webviewView) {
|
||||
webviewView.webview.onDidReceiveMessage(async (message) => {
|
||||
const handlers = {
|
||||
chat: () => this.chatService.handleChatMessage(webviewView, message.text, this.messageHistory, this.markdownRenderer, this.repoTracker),
|
||||
copyCode: () => this.uiManager.handleCopyCode(message.text),
|
||||
insertCode: () => this.uiManager.handleInsertCode(message.text),
|
||||
openFile: () => this.fileManager.openFileInEditor(message.path, message.line),
|
||||
getFileReferences: () => this.fileManager.handleFileReferenceRequest(webviewView, message.prefix),
|
||||
getGitStatus: () => this.gitManager.sendGitStatus(webviewView)
|
||||
};
|
||||
|
||||
const handler = handlers[message.command];
|
||||
if (handler) {
|
||||
await handler();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { AISidebarProvider };
|
|
@ -0,0 +1,69 @@
|
|||
// FileSystemManager.js
|
||||
const vscode = require("vscode");
|
||||
|
||||
class FileSystemManager {
|
||||
constructor(repoTracker) {
|
||||
this.repoTracker = repoTracker;
|
||||
}
|
||||
|
||||
async setupFileWatcher(webviewView, disposables) {
|
||||
await this._sendFileTree(webviewView);
|
||||
|
||||
const watcher = vscode.workspace.createFileSystemWatcher('**/*');
|
||||
['onDidChange', 'onDidCreate', 'onDidDelete'].forEach(event => {
|
||||
watcher[event](() => this._sendFileTree(webviewView));
|
||||
});
|
||||
|
||||
disposables.push(watcher);
|
||||
}
|
||||
|
||||
async _sendFileTree(webviewView) {
|
||||
const fileTree = await this.repoTracker.buildFileTree();
|
||||
webviewView.webview.postMessage({
|
||||
command: 'updateFileTree',
|
||||
fileTree: fileTree
|
||||
});
|
||||
}
|
||||
|
||||
async handleFileReferenceRequest(webviewView, prefix) {
|
||||
const fileTree = this.repoTracker.fileTree;
|
||||
const files = this._flattenFileTree(fileTree)
|
||||
.filter(file => file.path.toLowerCase().includes(prefix.toLowerCase()))
|
||||
.slice(0, 10);
|
||||
|
||||
webviewView.webview.postMessage({
|
||||
command: 'fileReferenceSuggestions',
|
||||
suggestions: files
|
||||
});
|
||||
}
|
||||
|
||||
_flattenFileTree(tree, path = '') {
|
||||
return tree.reduce((acc, item) => {
|
||||
if (item.type === 'file') {
|
||||
acc.push({ type: 'file', path: path + item.name, name: item.name });
|
||||
} else if (item.type === 'directory' && item.children) {
|
||||
acc.push(...this._flattenFileTree(item.children, path + item.name + '/'));
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async openFileInEditor(filePath, line) {
|
||||
try {
|
||||
const document = await vscode.workspace.openTextDocument(filePath);
|
||||
const editor = await vscode.window.showTextDocument(document);
|
||||
|
||||
if (line) {
|
||||
const lineNumber = parseInt(line) - 1;
|
||||
const range = editor.document.lineAt(lineNumber).range;
|
||||
editor.selection = new vscode.Selection(range.start, range.end);
|
||||
editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error opening file:', error);
|
||||
vscode.window.showErrorMessage(`Failed to open file: ${filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { FileSystemManager };
|
|
@ -0,0 +1,25 @@
|
|||
// GitManager.js
|
||||
class GitManager {
|
||||
constructor(repoTracker) {
|
||||
this.repoTracker = repoTracker;
|
||||
}
|
||||
|
||||
async setupGitWatcher(webviewView, disposables) {
|
||||
await this.sendGitStatus(webviewView);
|
||||
|
||||
const gitWatcher = await this.repoTracker.watchGitChanges();
|
||||
gitWatcher.onDidChange(() => this.sendGitStatus(webviewView));
|
||||
|
||||
disposables.push(gitWatcher);
|
||||
}
|
||||
|
||||
async sendGitStatus(webviewView) {
|
||||
const gitStatus = await this.repoTracker.getGitStatus();
|
||||
webviewView.webview.postMessage({
|
||||
command: 'updateGitStatus',
|
||||
gitStatus: gitStatus
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GitManager };
|
|
@ -0,0 +1,48 @@
|
|||
// MarkdownRenderer.js
|
||||
const MarkdownIt = require("markdown-it");
|
||||
const hljs = require("highlight.js");
|
||||
|
||||
class MarkdownRenderer {
|
||||
constructor() {
|
||||
this.md = new MarkdownIt({
|
||||
highlight: this._highlightCode.bind(this),
|
||||
html: true,
|
||||
linkify: true
|
||||
});
|
||||
}
|
||||
|
||||
_highlightCode(str, lang) {
|
||||
let highlighted;
|
||||
try {
|
||||
highlighted = lang && hljs.getLanguage(lang)
|
||||
? hljs.highlight(str, { language: lang }).value
|
||||
: this.md.utils.escapeHtml(str);
|
||||
} catch (err) {
|
||||
console.error("Syntax highlighting error:", err);
|
||||
highlighted = this.md.utils.escapeHtml(str);
|
||||
}
|
||||
|
||||
const escapedCode = this.md.utils.escapeHtml(str);
|
||||
return this._wrapCodeBlock(escapedCode, highlighted, lang);
|
||||
}
|
||||
|
||||
_wrapCodeBlock(escapedCode, highlighted, lang) {
|
||||
return `
|
||||
<div class="code-block-wrapper">
|
||||
<div class="code-block-header">
|
||||
<span class="code-language">${lang || 'text'}</span>
|
||||
<div class="code-block-buttons">
|
||||
<button class="code-button copy-button" data-code="${escapedCode}">Copy</button>
|
||||
<button class="code-button insert-button" data-code="${escapedCode}">Insert</button>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="hljs"><code class="language-${lang || 'text'}">${highlighted}</code></pre>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render(text) {
|
||||
return this.md.render(text);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MarkdownRenderer };
|
|
@ -0,0 +1,232 @@
|
|||
const vscode = require("vscode");
|
||||
const { exec } = require('child_process');
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const util = require('util');
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
class RepoTracker {
|
||||
constructor() {
|
||||
this.workspaceRoot = "";
|
||||
this.fileTree = [];
|
||||
this.gitInfo = null;
|
||||
this.ignoredPaths = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'dist',
|
||||
'build',
|
||||
'.next',
|
||||
'.cache'
|
||||
];
|
||||
this._initializeCompletionProvider();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders) return;
|
||||
|
||||
this.workspaceRoot = workspaceFolders[0].uri.fsPath;
|
||||
await this.buildFileTree();
|
||||
await this.initializeGitTracking();
|
||||
}
|
||||
|
||||
async initializeGitTracking() {
|
||||
try {
|
||||
const { stdout: gitRoot } = await execPromise('git rev-parse --show-toplevel', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
|
||||
this.gitInfo = {
|
||||
root: gitRoot.trim(),
|
||||
branch: '',
|
||||
lastCommit: '',
|
||||
status: []
|
||||
};
|
||||
|
||||
await this.updateGitStatus();
|
||||
} catch (error) {
|
||||
console.log('Not a git repository or git not installed: ', error);
|
||||
this.gitInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateGitStatus() {
|
||||
if (!this.gitInfo) return;
|
||||
|
||||
try {
|
||||
// Get current branch
|
||||
const { stdout: branch } = await execPromise('git branch --show-current', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
this.gitInfo.branch = branch.trim();
|
||||
|
||||
// Get last commit
|
||||
const { stdout: lastCommit } = await execPromise('git log -1 --oneline', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
this.gitInfo.lastCommit = lastCommit.trim();
|
||||
|
||||
// Get status
|
||||
const { stdout: status } = await execPromise('git status --porcelain', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
this.gitInfo.status = status.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => ({
|
||||
status: line.substring(0, 2).trim(),
|
||||
file: line.substring(3).trim()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error updating git status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
_initializeCompletionProvider() {
|
||||
// Register the @ completion provider
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
{ pattern: '**' },
|
||||
{
|
||||
provideCompletionItems: (document, position) => {
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
||||
|
||||
// Check if we're typing after an @
|
||||
if (!linePrefix.endsWith('@')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create completion items for all files in the workspace
|
||||
return this.fileTree
|
||||
.flat(Infinity)
|
||||
.filter(item => item.type === 'file')
|
||||
.map(file => {
|
||||
const completionItem = new vscode.CompletionItem(
|
||||
file.path,
|
||||
vscode.CompletionItemKind.File
|
||||
);
|
||||
|
||||
// When selected, it will insert the file path and prepare for line number
|
||||
completionItem.insertText = file.path + ':';
|
||||
completionItem.detail = `File: ${file.path}`;
|
||||
completionItem.command = {
|
||||
command: 'editor.action.triggerSuggest',
|
||||
title: 'Re-trigger completions'
|
||||
};
|
||||
|
||||
return completionItem;
|
||||
});
|
||||
}
|
||||
},
|
||||
'@' // Trigger character
|
||||
);
|
||||
|
||||
// Register the line number completion provider
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
{ pattern: '**' },
|
||||
{
|
||||
provideCompletionItems: async (document, position) => {
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
||||
|
||||
// Check if we're typing after a file path and colon
|
||||
const match = linePrefix.match(/@([^:]+):$/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const filePath = match[1];
|
||||
const content = await this.getFileContent(filePath);
|
||||
if (!content) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create completion items for each line number with preview
|
||||
const lines = content.split('\n');
|
||||
return lines.map((line, index) => {
|
||||
const lineNum = index + 1;
|
||||
const completionItem = new vscode.CompletionItem(
|
||||
`${lineNum}`,
|
||||
vscode.CompletionItemKind.Reference
|
||||
);
|
||||
|
||||
completionItem.detail = line.trim();
|
||||
completionItem.documentation = new vscode.MarkdownString(
|
||||
`Preview of line ${lineNum}:\n\`\`\`\n${line.trim()}\n\`\`\``
|
||||
);
|
||||
|
||||
return completionItem;
|
||||
});
|
||||
}
|
||||
},
|
||||
':' // Trigger character
|
||||
);
|
||||
}
|
||||
|
||||
async buildFileTree() {
|
||||
this.fileTree = await this.scanDirectory(this.workspaceRoot);
|
||||
}
|
||||
|
||||
async scanDirectory(dirPath) {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
const relativePath = path.relative(this.workspaceRoot, fullPath);
|
||||
|
||||
if (this.ignoredPaths.some(ignored => relativePath.includes(ignored))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const subFiles = await this.scanDirectory(fullPath);
|
||||
files.push({
|
||||
type: 'directory',
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
children: subFiles
|
||||
});
|
||||
} else {
|
||||
files.push({
|
||||
type: 'file',
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
extension: path.extname(entry.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async getFileContent(filePath) {
|
||||
const fullPath = path.join(this.workspaceRoot, filePath);
|
||||
try {
|
||||
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error(`Error reading file ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getFileTreeSummary() {
|
||||
return JSON.stringify(this.fileTree, null, 2);
|
||||
}
|
||||
getGitStatus() {
|
||||
return this.gitInfo;
|
||||
}
|
||||
|
||||
async watchGitChanges() {
|
||||
// Set up a file system watcher for git changes
|
||||
const gitWatcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(this.workspaceRoot, '.git/**')
|
||||
);
|
||||
|
||||
gitWatcher.onDidChange(() => this.updateGitStatus());
|
||||
gitWatcher.onDidCreate(() => this.updateGitStatus());
|
||||
gitWatcher.onDidDelete(() => this.updateGitStatus());
|
||||
|
||||
return gitWatcher;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RepoTracker };
|
|
@ -0,0 +1,62 @@
|
|||
// UIManager.js
|
||||
const vscode = require("vscode");
|
||||
const { styles } = require("../webview/sidebar/styles");
|
||||
const { scripts } = require("../webview/sidebar/scripts");
|
||||
|
||||
class UIManager {
|
||||
constructor(extensionUri) {
|
||||
this._extensionUri = extensionUri;
|
||||
}
|
||||
|
||||
initializeWebview(webviewView, extensionUri) {
|
||||
webviewView.webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [extensionUri]
|
||||
};
|
||||
|
||||
webviewView.webview.html = this._getWebviewContent();
|
||||
}
|
||||
|
||||
handleCopyCode(text) {
|
||||
vscode.env.clipboard.writeText(text);
|
||||
vscode.window.showInformationMessage('Code copied to clipboard!');
|
||||
}
|
||||
|
||||
handleInsertCode(code) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor) {
|
||||
editor.edit(editBuilder => {
|
||||
editBuilder.insert(editor.selection.active, code);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getWebviewContent() {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>${styles}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="gitStatus"></div>
|
||||
<div id="chatContainer" class="chat-container">
|
||||
<div id="messageContainer" class="message-container"></div>
|
||||
<div class="input-container">
|
||||
<input
|
||||
type="text"
|
||||
id="promptInput"
|
||||
placeholder="Type a message..."
|
||||
class="prompt-input"
|
||||
/>
|
||||
<button id="sendButton" class="send-button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>${scripts}</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UIManager };
|
|
@ -0,0 +1,231 @@
|
|||
const vscode = require("vscode");
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
class RepoTracker {
|
||||
constructor() {
|
||||
this.workspaceRoot = "";
|
||||
this.fileTree = [];
|
||||
this.gitInfo = null;
|
||||
this.ignoredPaths = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'dist',
|
||||
'build',
|
||||
'.next',
|
||||
'.cache'
|
||||
];
|
||||
this._initializeCompletionProvider();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
if (!workspaceFolders) return;
|
||||
|
||||
this.workspaceRoot = workspaceFolders[0].uri.fsPath;
|
||||
await this.buildFileTree();
|
||||
await this.initializeGitTracking();
|
||||
}
|
||||
|
||||
async initializeGitTracking() {
|
||||
try {
|
||||
const { stdout: gitRoot } = await execPromise('git rev-parse --show-toplevel', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
|
||||
this.gitInfo = {
|
||||
root: gitRoot.trim(),
|
||||
branch: '',
|
||||
lastCommit: '',
|
||||
status: []
|
||||
};
|
||||
|
||||
await this.updateGitStatus();
|
||||
} catch (error) {
|
||||
console.log('Not a git repository or git not installed');
|
||||
this.gitInfo = null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateGitStatus() {
|
||||
if (!this.gitInfo) return;
|
||||
|
||||
try {
|
||||
// Get current branch
|
||||
const { stdout: branch } = await execPromise('git branch --show-current', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
this.gitInfo.branch = branch.trim();
|
||||
|
||||
// Get last commit
|
||||
const { stdout: lastCommit } = await execPromise('git log -1 --oneline', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
this.gitInfo.lastCommit = lastCommit.trim();
|
||||
|
||||
// Get status
|
||||
const { stdout: status } = await execPromise('git status --porcelain', {
|
||||
cwd: this.workspaceRoot
|
||||
});
|
||||
this.gitInfo.status = status.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => ({
|
||||
status: line.substring(0, 2).trim(),
|
||||
file: line.substring(3).trim()
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error updating git status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
_initializeCompletionProvider() {
|
||||
// Register the @ completion provider
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
{ pattern: '**' },
|
||||
{
|
||||
provideCompletionItems: (document, position) => {
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
||||
|
||||
// Check if we're typing after an @
|
||||
if (!linePrefix.endsWith('@')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create completion items for all files in the workspace
|
||||
return this.fileTree
|
||||
.flat(Infinity)
|
||||
.filter(item => item.type === 'file')
|
||||
.map(file => {
|
||||
const completionItem = new vscode.CompletionItem(
|
||||
file.path,
|
||||
vscode.CompletionItemKind.File
|
||||
);
|
||||
|
||||
// When selected, it will insert the file path and prepare for line number
|
||||
completionItem.insertText = file.path + ':';
|
||||
completionItem.detail = `File: ${file.path}`;
|
||||
completionItem.command = {
|
||||
command: 'editor.action.triggerSuggest',
|
||||
title: 'Re-trigger completions'
|
||||
};
|
||||
|
||||
return completionItem;
|
||||
});
|
||||
}
|
||||
},
|
||||
'@' // Trigger character
|
||||
);
|
||||
|
||||
// Register the line number completion provider
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
{ pattern: '**' },
|
||||
{
|
||||
provideCompletionItems: async (document, position) => {
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character);
|
||||
|
||||
// Check if we're typing after a file path and colon
|
||||
const match = linePrefix.match(/@([^:]+):$/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const filePath = match[1];
|
||||
const content = await this.getFileContent(filePath);
|
||||
if (!content) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create completion items for each line number with preview
|
||||
const lines = content.split('\n');
|
||||
return lines.map((line, index) => {
|
||||
const lineNum = index + 1;
|
||||
const completionItem = new vscode.CompletionItem(
|
||||
`${lineNum}`,
|
||||
vscode.CompletionItemKind.Reference
|
||||
);
|
||||
|
||||
completionItem.detail = line.trim();
|
||||
completionItem.documentation = new vscode.MarkdownString(
|
||||
`Preview of line ${lineNum}:\n\`\`\`\n${line.trim()}\n\`\`\``
|
||||
);
|
||||
|
||||
return completionItem;
|
||||
});
|
||||
}
|
||||
},
|
||||
':' // Trigger character
|
||||
);
|
||||
}
|
||||
|
||||
// ... (keep existing methods: buildFileTree, scanDirectory, getFileContent, getFileTreeSummary)
|
||||
async buildFileTree() {
|
||||
this.fileTree = await this.scanDirectory(this.workspaceRoot);
|
||||
}
|
||||
|
||||
async scanDirectory(dirPath) {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
const relativePath = path.relative(this.workspaceRoot, fullPath);
|
||||
|
||||
if (this.ignoredPaths.some(ignored => relativePath.includes(ignored))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const subFiles = await this.scanDirectory(fullPath);
|
||||
files.push({
|
||||
type: 'directory',
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
children: subFiles
|
||||
});
|
||||
} else {
|
||||
files.push({
|
||||
type: 'file',
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
extension: path.extname(entry.name)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async getFileContent(filePath) {
|
||||
const fullPath = path.join(this.workspaceRoot, filePath);
|
||||
try {
|
||||
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
||||
return content;
|
||||
} catch (error) {
|
||||
console.error(`Error reading file ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getFileTreeSummary() {
|
||||
return JSON.stringify(this.fileTree, null, 2);
|
||||
}
|
||||
getGitStatus() {
|
||||
return this.gitInfo;
|
||||
}
|
||||
|
||||
async watchGitChanges() {
|
||||
// Set up a file system watcher for git changes
|
||||
const gitWatcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(this.workspaceRoot, '.git/**')
|
||||
);
|
||||
|
||||
gitWatcher.onDidChange(() => this.updateGitStatus());
|
||||
gitWatcher.onDidCreate(() => this.updateGitStatus());
|
||||
gitWatcher.onDidDelete(() => this.updateGitStatus());
|
||||
|
||||
return gitWatcher;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RepoTracker };
|
|
@ -0,0 +1,124 @@
|
|||
// scripts.js
|
||||
exports.scripts = /*javascript*/`
|
||||
let fileTree = [];
|
||||
let gitStatus = null;
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
// Handle file reference suggestions
|
||||
promptInput.addEventListener('input', (e) => {
|
||||
const text = e.target.value;
|
||||
const lastAtSign = text.lastIndexOf('@');
|
||||
if (lastAtSign !== -1) {
|
||||
const prefix = text.substring(lastAtSign + 1);
|
||||
vscode.postMessage({
|
||||
command: 'getFileReferences',
|
||||
prefix: prefix
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle git status updates
|
||||
window.addEventListener('message', event => {
|
||||
const message = event.data;
|
||||
switch (message.command) {
|
||||
case 'updateGitStatus':
|
||||
gitStatus = message.gitStatus;
|
||||
updateGitStatusDisplay();
|
||||
break;
|
||||
case 'updateFileTree':
|
||||
fileTree = message.fileTree;
|
||||
break;
|
||||
case 'fileReferenceSuggestions':
|
||||
showFileReferenceSuggestions(message.suggestions);
|
||||
break;
|
||||
case 'updateBotMessage':
|
||||
updateChatDisplay(message.html);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function updateGitStatusDisplay() {
|
||||
if (!gitStatus) return;
|
||||
|
||||
const statusEl = document.getElementById('gitStatus');
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = \`
|
||||
<div class="git-info">
|
||||
<span>Branch: \${gitStatus.branch}</span>
|
||||
<span>Last commit: \${gitStatus.lastCommit}</span>
|
||||
</div>
|
||||
\`;
|
||||
}
|
||||
}
|
||||
|
||||
function showFileReferenceSuggestions(suggestions) {
|
||||
let suggestionsDiv = document.getElementById('suggestions');
|
||||
if (!suggestionsDiv) {
|
||||
suggestionsDiv = document.createElement('div');
|
||||
suggestionsDiv.id = 'suggestions';
|
||||
suggestionsDiv.className = 'suggestions';
|
||||
document.body.appendChild(suggestionsDiv);
|
||||
}
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
suggestionsDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = promptInput.getBoundingClientRect();
|
||||
suggestionsDiv.style.position = 'absolute';
|
||||
suggestionsDiv.style.left = rect.left + 'px';
|
||||
suggestionsDiv.style.top = (rect.bottom + 5) + 'px';
|
||||
suggestionsDiv.style.display = 'block';
|
||||
|
||||
suggestionsDiv.innerHTML = suggestions.map(file => \`
|
||||
<div class="suggestion" data-path="\${file.path}">
|
||||
\${file.path}
|
||||
</div>
|
||||
\`).join('');
|
||||
|
||||
suggestionsDiv.querySelectorAll('.suggestion').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = el.dataset.path;
|
||||
const cursorPos = promptInput.selectionStart;
|
||||
const textBeforeCursor = promptInput.value.substring(0, cursorPos);
|
||||
const lastAtSign = textBeforeCursor.lastIndexOf('@');
|
||||
const textAfterCursor = promptInput.value.substring(cursorPos);
|
||||
|
||||
promptInput.value = textBeforeCursor.substring(0, lastAtSign) +
|
||||
'@' + path + ':' + textAfterCursor;
|
||||
suggestionsDiv.style.display = 'none';
|
||||
promptInput.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateChatDisplay(html) {
|
||||
const chatContainer = document.getElementById('chatContainer');
|
||||
if (chatContainer) {
|
||||
chatContainer.innerHTML = html;
|
||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle code actions
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('copy-button')) {
|
||||
vscode.postMessage({
|
||||
command: 'copyCode',
|
||||
text: e.target.dataset.code
|
||||
});
|
||||
} else if (e.target.classList.contains('insert-button')) {
|
||||
vscode.postMessage({
|
||||
command: 'insertCode',
|
||||
text: e.target.dataset.code
|
||||
});
|
||||
}
|
||||
|
||||
// Hide suggestions when clicking outside
|
||||
const suggestionsDiv = document.getElementById('suggestions');
|
||||
if (suggestionsDiv && !suggestionsDiv.contains(e.target) && e.target !== promptInput) {
|
||||
suggestionsDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
`;
|
|
@ -0,0 +1,99 @@
|
|||
// styles.js
|
||||
exports.styles = /*css*/ `
|
||||
.git-info {
|
||||
padding: 8px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.git-info span {
|
||||
margin-right: 16px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
position: absolute;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: var(--border-radius);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggestion:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-block-wrapper {
|
||||
margin: 16px 0;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: var(--vscode-editor-inactiveSelectionBackground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.code-language {
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: 0.9em;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.code-block-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.code-button {
|
||||
padding: 4px 8px;
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.code-button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
pre.hljs {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,29 @@
|
|||
const getBaseStyles = require('./styles');
|
||||
const getBaseScripts = require('./scripts');
|
||||
|
||||
function getWebviewContent() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/vs2015.min.css">
|
||||
<style>
|
||||
${getBaseStyles()}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="gitStatus"></div>
|
||||
<div id="chatContainer"></div>
|
||||
<div class="input-container">
|
||||
<textarea id="prompt" placeholder="Type your message... Use @ to reference files"></textarea>
|
||||
<button id="send">Send</button>
|
||||
</div>
|
||||
<script>
|
||||
${getBaseScripts()}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
module.exports = getWebviewContent;
|
|
@ -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';
|
||||
// const myExtension = require('../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));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
# Welcome to your VS Code Extension
|
||||
|
||||
## What's in the folder
|
||||
|
||||
* This folder contains all of the files necessary for your extension.
|
||||
* `package.json` - this is the manifest file in which you declare your extension and command.
|
||||
* The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
|
||||
* `extension.js` - this is the main file where you will provide the implementation of your command.
|
||||
* The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
|
||||
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
|
||||
|
||||
## Get up and running straight away
|
||||
|
||||
* Press `F5` to open a new window with your extension loaded.
|
||||
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
|
||||
* Set breakpoints in your code inside `extension.js` to debug your extension.
|
||||
* Find output from your extension in the debug console.
|
||||
|
||||
## Make changes
|
||||
|
||||
* You can relaunch the extension from the debug toolbar after changing code in `extension.js`.
|
||||
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
|
||||
|
||||
## Explore the API
|
||||
|
||||
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
|
||||
|
||||
## Run tests
|
||||
|
||||
* Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner)
|
||||
* Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A`
|
||||
* See the output of the test result in the Test Results view.
|
||||
* Make changes to `test/extension.test.js` or create new test files inside the `test` folder.
|
||||
* The provided test runner will only consider files matching the name pattern `**.test.js`.
|
||||
* You can create folders inside the `test` folder to structure your tests any way you want.
|
||||
|
||||
## Go further
|
||||
|
||||
* [Follow UX guidelines](https://code.visualstudio.com/api/ux-guidelines/overview) to create extensions that seamlessly integrate with VS Code's native interface and patterns.
|
||||
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace.
|
||||
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).
|
||||
* Integrate to the [report issue](https://code.visualstudio.com/api/get-started/wrapping-up#issue-reporting) flow to get issue and feature requests reported by users.
|
Loading…
Reference in New Issue