```
├── .gitignore (100 tokens)
├── .vscode-test.mjs
├── .vscodeignore
├── CHANGELOG.md
├── LICENSE (omitted)
├── README.md (400 tokens)
├── assets/
├── logo.png
├── doc/
├── expot.gif
├── favoites.gif
├── multi.gif
├── open.gif
├── oupput.png
├── select.gif
├── eslint.config.mjs (100 tokens)
├── package.json (600 tokens)
├── src/
├── constant.ts (100 tokens)
├── databaseEditor.ts (1500 tokens)
├── dispose.ts (100 tokens)
├── errorCode.ts (100 tokens)
├── eventService.ts
├── extension.ts (300 tokens)
├── libs/
├── log.service.ts
├── utils.ts (1200 tokens)
├── webviewDialog.ts (700 tokens)
├── server/
├── api.ts (1700 tokens)
├── dbServer.ts (900 tokens)
├── sqlite3/
├── lib/
├── bindings.js (1200 tokens)
├── sqlite3-binding.js (100 tokens)
├── sqlite3.d.ts (omitted)
├── sqlite3.js (1300 tokens)
├── trace.js (300 tokens)
├── test/
├── extension.test.ts (100 tokens)
├── typings.d.ts (omitted)
├── tsconfig.json (100 tokens)
├── ui/
├── README.md (700 tokens)
├── package.json (300 tokens)
├── public/
├── favicon.ico
├── index.html (500 tokens)
├── logo192.png
├── logo512.png
├── manifest.json (100 tokens)
├── robots.txt
├── src/
├── App.css (200 tokens)
├── App.js (300 tokens)
├── App.test.js
├── Layout.jsx (100 tokens)
├── constant.js
├── features/
├── sqlEditor/
├── Editor/
├── SqlEditor.jsx (1000 tokens)
├── index.jsx (400 tokens)
├── utils.js (400 tokens)
├── Favorite/
├── AddOrEditModal.jsx (500 tokens)
├── index.jsx (1300 tokens)
├── QueryResult/
├── index.jsx (1200 tokens)
├── TableTree/
├── index.jsx (400 tokens)
├── index.jsx (500 tokens)
├── index.css (100 tokens)
├── index.js (100 tokens)
├── logo.svg (500 tokens)
├── reportWebVitals.js (100 tokens)
├── setupTests.js
├── stores/
├── api.js (300 tokens)
├── homeSlice.js (100 tokens)
├── index.js
├── rpcRequest.js (300 tokens)
├── sqlSlice.js (700 tokens)
├── utils.js (200 tokens)
├── vsc-extension-quickstart.md (600 tokens)
├── webpack.config.js (400 tokens)
```
## /.gitignore
```gitignore path="/.gitignore"
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.eslintcache
yarn.lock
/ui_dist
/.history
/.vscode
package-lock.json
*.vsix
/ui/node_modules
/ui/build
/ui/package-lock.json
```
## /.vscode-test.mjs
```mjs path="/.vscode-test.mjs"
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'out/test/**/*.test.js',
});
```
## /.vscodeignore
```vscodeignore path="/.vscodeignore"
.vscode/**
.vscode-test/**
out/**
node_modules/**
src/**
.gitignore
.yarnrc
webpack.config.js
vsc-extension-quickstart.md
**/tsconfig.json
**/eslint.config.mjs
**/*.map
**/*.ts
**/.vscode-test.*
ui/**
.history/**
doc/**
```
## /CHANGELOG.md
# Change Log
All notable changes to the "free-sqlite" 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
## /README.md
# Free sqlite
VSCode extension to explore and query SQLite databases.
open-source and free.
## ✨ Features
- 📃 Open any SQLite file - Simply click on .sqlite or .db files to open them instantly
- 🛡 Table Explorer - Browse all tables in your database from an integrated sidebar
- 🌈 Data Visualization - View table data in a clean, modern interface that adapts to your VS Code theme
- 🖥 SQL automatically complete - SQL statement editor, Automatically complete SQL keywords, table names, and fields
- ❌ SQL error info - Execute query error information output display
- 📦 Query result export - The query results can be exported as Excel or CSV
- ❤️ Favorites - Collect some of your most commonly used SQL statements
## How to use
### Install
VSCode extension install!
[Install free sqlite](https://marketplace.visualstudio.com/items?itemName=free-sqlite.free-sqlite)
### Open database
Now! Browse all tables in your database in the right panel

### Multiple files

### SQL Editor
Automatically complete SQL keywords, table names, and fields

### SQL Query error output
If there is an error when running SQL, you can see the error log through the following channels
Terminal>output>free sqlite

### Favorite
Quickly execute or view your frequently used SQL

### Export

## /assets/logo.png
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/assets/logo.png
## /doc/expot.gif
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/doc/expot.gif
## /doc/favoites.gif
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/doc/favoites.gif
## /doc/multi.gif
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/doc/multi.gif
## /doc/open.gif
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/doc/open.gif
## /doc/oupput.png
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/doc/oupput.png
## /doc/select.gif
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/doc/select.gif
## /eslint.config.mjs
```mjs path="/eslint.config.mjs"
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
export default [{
files: ["**/*.ts"],
}, {
plugins: {
"@typescript-eslint": typescriptEslint,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: "module",
},
rules: {
"@typescript-eslint/naming-convention": ["warn", {
selector: "import",
format: ["camelCase", "PascalCase"],
}],
curly: "warn",
eqeqeq: "warn",
"no-throw-literal": "warn",
semi: "warn",
},
}];
```
## /package.json
```json path="/package.json"
{
"name": "free-sqlite",
"displayName": "free sqlite",
"description": "SQLite tools for VSCode",
"icon": "assets/logo.png",
"version": "0.6.20",
"homepage": "https://callfeel.com",
"repository": {
"type": "git",
"url": "https://github.com/fjb040911/free-sqlite"
},
"engines": {
"vscode": "^1.91.0"
},
"publisher": "free-sqlite",
"categories": [
"Databases",
"Notebooks",
"Programming Languages",
"Formatters"
],
"tags": [
"sqlite",
"sqlite3",
"sql",
"database",
"export",
"view"
],
"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
"languages": [
{
"id": "sql",
"extensions": [
".sqlite",
".sqlite3",
".db",
".db3",
".sdb",
".s3db",
".gpkg"
]
}
],
"customEditors": [
{
"viewType": "free-sqlite.view",
"displayName": "Free SQLite",
"selector": [
{
"filenamePattern": "*.sqlite"
},
{
"filenamePattern": "*.sqlite3"
},
{
"filenamePattern": "*.db"
},
{
"filenamePattern": "*.db3"
},
{
"filenamePattern": "*.sdb"
},
{
"filenamePattern": "*.s3db"
}
]
}
]
},
"scripts": {
"vscode:prepublish": "npm run 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": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src",
"test": "vscode-test",
"rebuild-db": "cd node_modules/sqlite3 && npm run rebuild",
"vsce-package": "npm run rm-map && vsce package",
"rm-map": "find ui_dist -name '*.map' -delete",
"rm-his": "rm -r -f .history",
"rm-uidist": "cd ui_dist && rm -rf * && cd ..",
"mac-package": "npm run rm-uidist && npm run compile && npm run rm-his && npm run rm-map && npm run vsce-package"
},
"devDependencies": {
"@types/mocha": "^10.0.10",
"@types/node": "22.x",
"@types/vscode": "^1.91.0",
"@typescript-eslint/eslint-plugin": "^8.39.0",
"@typescript-eslint/parser": "^8.39.0",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"eslint": "^9.32.0",
"ts-loader": "^9.5.2",
"typescript": "^5.9.2",
"webpack": "^5.101.0",
"webpack-cli": "^6.0.1"
},
"dependencies": {
"@types/lodash": "^4.17.20",
"copy-webpack-plugin": "^13.0.1",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"sqlite3": "^5.1.7",
"uuid": "^11.1.0"
}
}
```
## /src/constant.ts
```ts path="/src/constant.ts"
/* eslint-disable */
import * as path from 'path';
export const methods = {
PAGE_STATUS: 'PAGE_STATUS',
ROUTER: 'ROUTER',
CHANGE_THEME: 'CHANGE_THEME',
GET_ALL_TABLES: 'GET_ALL_TABLES',
EXECUTE_SQL: 'EXECUTE_SQL',
GET_USER_FAVORITES: 'GET_USER_FAVORITES',
ADD_USER_FAVORITE: 'ADD_USER_FAVORITE',
RENAME_USER_FAVORITE: 'RENAME_USER_FAVORITE',
REMOVE_USER_FAVORITE: 'REMOVE_USER_FAVORITE',
COPY_TO_CLIPBOARD: 'COPY_TO_CLIPBOARD',
EXPORT_SQL_RESULT: 'EXPORT_SQL_RESULT',
SORT_USER_FAVORITES: 'SORT_USER_FAVORITES'
}
export const events = {
CHANGE_THEME: 'CHANGE_THEME'
}
export const uiDir = () => path.resolve(__dirname, `../ui_dist/ui/build`);
```
## /src/databaseEditor.ts
```ts path="/src/databaseEditor.ts"
import * as vscode from 'vscode';
import { parse } from 'path';
import { Disposable } from './dispose';
import { WebviewDialog } from './libs/webviewDialog';
import { handleMessages } from './server/api';
const md5 = require('md5');
interface SqliteDocumentDelegate {
getDB(): any;
}
/**
* Define the document (the data model) used for Sqlite files.
*/
class SqliteDocument extends Disposable implements vscode.CustomDocument {
static async create(
uri: vscode.Uri,
backupId: string | undefined,
delegate: SqliteDocumentDelegate,
): Promise<SqliteDocument | PromiseLike<SqliteDocument>> {
return new SqliteDocument(uri, delegate);
}
private readonly _uri: vscode.Uri;
private readonly _delegate: SqliteDocumentDelegate;
private constructor(
uri: vscode.Uri,
delegate: SqliteDocumentDelegate
) {
super();
this._uri = uri;
this._delegate = delegate;
}
public get uri() { return this._uri; }
private readonly _onDidDispose = this._register(new vscode.EventEmitter<void>());
/**
* Fired when the document is disposed of.
*/
public readonly onDidDispose = this._onDidDispose.event;
// private readonly _onDidChangeDocument = this._register(new vscode.EventEmitter<{
// readonly content?: Uint8Array;
// readonly edits: readonly PawDrawEdit[];
// }>());
/**
* Fired to notify webviews that the document has changed.
*/
// public readonly onDidChangeContent = this._onDidChangeDocument.event;
private readonly _onDidChange = this._register(new vscode.EventEmitter<{
readonly label: string,
undo(): void,
redo(): void,
}>());
/**
* Fired to tell VS Code that an edit has occurred in the document.
*
* This updates the document's dirty indicator.
*/
public readonly onDidChange = this._onDidChange.event;
/**
* Called by VS Code when there are no more references to the document.
*
* This happens when all editors for it have been closed.
*/
dispose(): void {
this._onDidDispose.fire();
super.dispose();
}
/**
* Called when the user edits the document in a webview.
*
* This fires an event to notify VS Code that the document has been edited.
*/
makeEdit() {
}
/**
* Called by VS Code when the user saves the document.
*/
async save(cancellation: vscode.CancellationToken): Promise<void> {
}
/**
* Called by VS Code when the user saves the document to a new location.
*/
async saveAs(targetResource: vscode.Uri, cancellation: vscode.CancellationToken): Promise<void> {
}
/**
* Called by VS Code when the user calls `revert` on a document.
*/
async revert(_cancellation: vscode.CancellationToken): Promise<void> {
}
/**
* Called by VS Code to backup the edited document.
*
* These backups are used to implement hot exit.
*/
async backup(destination: vscode.Uri, cancellation: vscode.CancellationToken): Promise<vscode.CustomDocumentBackup> {
return {
id: destination.toString(),
delete: async () => {
}
};
}
}
/**
* Provider for sqlite editors.
*
* Used for `.db` files extension.
*
* This provider demonstrates:
*
* - How to implement a custom editor for binary files.
* - Setting up the initial webview for a custom editor.
* - Loading scripts and styles in a custom editor.
* - Communication between VS Code and the custom editor.
* - Using CustomDocuments to store information that is shared between multiple custom editors.
* - Implementing save, undo, redo, and revert.
* - Backing up a custom editor.
*/
export class SqliteEditorProvider implements vscode.CustomEditorProvider<SqliteDocument> {
public static register(context: vscode.ExtensionContext, resourceRootDir: string, router: string): vscode.Disposable {
return vscode.window.registerCustomEditorProvider(
SqliteEditorProvider.viewType,
new SqliteEditorProvider(context, resourceRootDir, router),
{
// For this demo extension, we enable `retainContextWhenHidden` which keeps the
// webview alive even when it is not visible. You should avoid using this setting
// unless is absolutely required as it does have memory overhead.
webviewOptions: {
retainContextWhenHidden: true,
},
supportsMultipleEditorsPerDocument: false,
}
);
}
private static readonly viewType = 'free-sqlite.view';
/**
* Tracks all known webviews
*/
private readonly webviews = new WebviewCollection();
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly resourceRootDir: string,
private readonly router: string
) { }
//#region CustomEditorProvider
async openCustomDocument(
uri: vscode.Uri,
openContext: { backupId?: string },
_token: vscode.CancellationToken
): Promise<SqliteDocument> {
const document: SqliteDocument = await SqliteDocument.create(uri, openContext.backupId, {
getDB: async () => {
}
});
console.log('uri', uri);
document.onDidDispose(() => {});
return document;
}
async resolveCustomEditor(
document: SqliteDocument,
webviewPanel: vscode.WebviewPanel,
_token: vscode.CancellationToken
): Promise<void> {
// Add the webview to our internal set of active webviews
console.log('resolveCustomEditor', webviewPanel);
const pathInfo = parse(document.uri.path);
let title = pathInfo.name;
const pathHash = md5(document.uri.path);
const tempPanel = new WebviewDialog(
SqliteEditorProvider.viewType,
this.resourceRootDir,
document.uri.path,
webviewPanel,
title,
'database',
handleMessages
);
this.webviews.add(pathHash, document.uri, tempPanel.getPanel());
}
private readonly _onDidChangeCustomDocument = new vscode.EventEmitter<vscode.CustomDocumentEditEvent<SqliteDocument>>();
public readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event;
public saveCustomDocument(document: SqliteDocument, cancellation: vscode.CancellationToken): Thenable<void> {
return document.save(cancellation);
}
public saveCustomDocumentAs(document: SqliteDocument, destination: vscode.Uri, cancellation: vscode.CancellationToken): Thenable<void> {
return document.saveAs(destination, cancellation);
}
public revertCustomDocument(document: SqliteDocument, cancellation: vscode.CancellationToken): Thenable<void> {
return document.revert(cancellation);
}
public backupCustomDocument(document: SqliteDocument, context: vscode.CustomDocumentBackupContext, cancellation: vscode.CancellationToken): Thenable<vscode.CustomDocumentBackup> {
return document.backup(context.destination, cancellation);
}
//#endregion
private _requestId = 1;
private readonly _callbacks = new Map<number, (response: any) => void>();
}
/**
* Tracks all webviews.
*/
class WebviewCollection {
private readonly _webviews = new Set<{
readonly pathHash: string;
readonly resource: string;
readonly webviewPanel: vscode.WebviewPanel;
}>();
/**
* Get all known webviews for a given uri.
*/
public *get(pathHash: string): Iterable<vscode.WebviewPanel> {
for (const entry of this._webviews) {
if (entry.pathHash === pathHash) {
yield entry.webviewPanel;
}
}
}
/**
* Add a new webview to the collection.
*/
public add(pathHash: string, uri: vscode.Uri, webviewPanel: vscode.WebviewPanel) {
const entry = { pathHash, resource: uri.toString(), webviewPanel };
this._webviews.add(entry);
webviewPanel.onDidDispose(() => {
console.log('webviewPanel.onDidDispose', uri.toString());
this._webviews.delete(entry);
});
}
}
```
## /src/dispose.ts
```ts path="/src/dispose.ts"
import * as vscode from 'vscode';
export function disposeAll(disposables: vscode.Disposable[]): void {
while (disposables.length) {
const item = disposables.pop();
if (item) {
item.dispose();
}
}
}
export abstract class Disposable {
private _isDisposed = false;
protected _disposables: vscode.Disposable[] = [];
public dispose() {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
disposeAll(this._disposables);
}
protected _register<T extends vscode.Disposable>(value: T): T {
if (this._isDisposed) {
value.dispose();
} else {
this._disposables.push(value);
}
return value;
}
protected get isDisposed(): boolean {
return this._isDisposed;
}
}
```
## /src/errorCode.ts
```ts path="/src/errorCode.ts"
export const errorCode: { [key: string]: number } = {
"database_not_opened": 4002,
"data_no_found": 4003,
"update_fail_no_found": 5003,
"required_parameter_missing": 5005,
"data_already_exist": 5009,
"no_save_path": 5010,
"file_not_exist": 5008,
"db_quey_error": 5017,
};
```
## /src/eventService.ts
```ts path="/src/eventService.ts"
import { EventEmitter } from 'events';
class SystemEmitter extends EventEmitter {};
const mEmitter = new SystemEmitter();
export default mEmitter;
```
## /src/extension.ts
```ts path="/src/extension.ts"
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';
import * as path from 'path';
import { events } from './constant';
import { SqliteEditorProvider } from './databaseEditor';
import eventService from './eventService';
const uiDir = path.resolve(__dirname, '../ui_dist');
// 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: vscode.ExtensionContext) {
// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log('Congratulations, your extension "free-sqlite" is now active!');
context.subscriptions.push(
SqliteEditorProvider.register(context, uiDir, '/'),
vscode.workspace.onDidChangeConfiguration((e) => {
// console.log('onDidChangeConfiguration----', e, e.affectsConfiguration('workbench.colorTheme'));
if (e.affectsConfiguration('workbench.colorTheme')) {
eventService.emit(events.CHANGE_THEME);
}
})
);
}
// This method is called when your extension is deactivated
export function deactivate() {}
```
## /src/libs/log.service.ts
```ts path="/src/libs/log.service.ts"
import { window } from 'vscode';
const packageInfo = require('../../package.json');
const logOutput = window.createOutputChannel(packageInfo.name, { log: true });
export default logOutput;
```
## /src/libs/utils.ts
```ts path="/src/libs/utils.ts"
import { Webview, Uri, workspace } from "vscode";
import { mkdir } from 'fs/promises';
import { existsSync } from 'fs';
import * as path from 'path';
import { platform } from 'node:process';
import { set, keys } from "lodash";
const { exec } = require('child_process');
const userHome = process.env.HOME||process.env.USERPROFILE||'';
export function getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
export function fixDynamicsSrc(html: string, assetManifest: any){
for(let scripName in assetManifest) {
if(scripName.endsWith('.js')) {
html = html.replace(scripName, assetManifest[scripName]);
}
}
return html;
}
/**
* 替换 href="./file" or src="./file" 并且找到静态资源
*/
export function fixResourceReferences(html: string, resourceRootDir: string, webview: Webview): string {
// const refRegex = /((href)|(src))="(\.\/[^"]+)"/g;
const refRegex = /((href)|(src))="([^"]+)"/g;
let refMatch;
while ((refMatch = refRegex.exec(html)) !== null) {
const offset = refMatch.index;
const length = refMatch[0].length;
const refAttr = refMatch[1];
const refName = refMatch[4];
const refPath = path.join(resourceRootDir, refName);
const refUri = webview.asWebviewUri(Uri.file(refPath));
const refReplace = refAttr + "=\"" + refUri + "\"";
html = html.slice(0, offset) + refReplace + html.slice(offset + length);
}
return html;
}
/**
* 替换 ${webview.cspSource} .
*/
export function fixCspSourceReferences(html: string, webview: Webview): string {
const cspSourceRegex = /\${webview.cspSource}/g;
let cspSourceMatch;
while ((cspSourceMatch = cspSourceRegex.exec(html)) !== null) {
html = html.slice(0, cspSourceMatch.index) + webview.cspSource +
html.slice(cspSourceMatch.index + cspSourceMatch[0].length);
}
return html;
}
export function responseData(id?: string, data?: any, ok?: boolean): API.ResponseResult {
return {
response: true,
id,
data,
ok
};
}
export function notificationData(method: string, data: any){
return {
notification: true,
method,
data
};
}
// export function csvJSON(csv: string){
// var lines=csv.split("\n");
// var result = [];
// var headers=lines[0].split(",");
// for(var i=1;i<lines.length;i++){
// var obj = {};
// var currentline=lines[i].split(",");
// for(var j=0;j<headers.length;j++){
// obj[headers[j] as keyof typeof obj] = currentline[j];
// }
// result.push(obj);
// }
// //return result; //JavaScript object
// return JSON.stringify(result); //JSON
// }
function isWindows() {
console.log(`This platform is ${platform}`);
return 'win32' === platform;
}
/**
* 格式化路径
* @param uri
*/
export function formartUri(uri: string){
if(isWindows()) {
if(uri.startsWith('/')){
return uri.substring(1);
}
}
return uri;
}
/**
* json转换成扁平化的数据结构
* 迭代2层
* @param {*} jsonData
*/
export const jsonToFormData = (jsonData: any, separator='.')=>{
const fkeys = keys(jsonData);
let resut: any = {};
for(let i = 0; i<fkeys.length; i++) {
const temp = jsonData[fkeys[i]];
const iType = typeof temp;
if(!temp || iType ==='string' || iType === 'number' || Array.isArray(temp) || iType === 'boolean') {
// 这几类可以不用继续迭代
resut = set(resut, fkeys[i], temp);
} else if(iType === 'object') {
// 再迭代一层
const subkeys = keys(temp);
for(let p = 0; p<subkeys.length; p++) {
resut[`${fkeys[i]}${separator}${subkeys[p]}`] = temp[subkeys[p]];
}
}
}
return resut;
};
export const getExt = (filePath: string) => {
const index= filePath.lastIndexOf(".");
//获取后缀
return filePath.substring(index+1);
};
export const isAssetTypeAnImage = (ext: string) => {
return [
'png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff'].
indexOf(ext.toLowerCase()) !== -1;
};
export const isImg = (filePath: string) => {
const ext = getExt(filePath);
return isAssetTypeAnImage(ext);
};
export const getTheme = () => {
const workbench = workspace.getConfiguration('workbench') || {};
return (workbench.colorTheme || '').toLowerCase().includes('light')
? 'light' : 'dark';
};
const getSysDir = ()=>{
return path.join(userHome, 'freeSqlite');
};
export const createSysDir = async ()=>{
const sysDir = getSysDir();
const appExists = await existsSync(sysDir);
if (!appExists) {
await mkdir(sysDir);
}
};
export function sysDbDir(): string{
return path.join(getSysDir(), 'freeSqlite.db');
};
export function openFile(path: string) {
let command;
if (process.platform === 'win32') {
command = `start "" "${path}"`; // Windows
} else if (process.platform === 'darwin') {
command = `open "${path}"`; // macOS
} else if (process.platform === 'linux') {
command = `xdg-open "${path}"`; // Linux
} else {
console.log('Unsupported platform');
return;
}
exec(command, (error: any, stdout: any, stderr: any) => {
if (error) {
console.error(`Error opening folder: ${error}`);
return;
}
console.log(`Folder opened successfully: ${stdout}`);
});
}
```
## /src/libs/webviewDialog.ts
```ts path="/src/libs/webviewDialog.ts"
/*
* @Author: Jianbing.fang
* @Date: 2022-02-17 14:56:20
* @LastEditTime: 2022-05-29 22:59:01
*/
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import eventService from '../eventService';
import { events } from '../constant';
import { fixResourceReferences, fixCspSourceReferences, notificationData, getTheme } from './utils';
const md5 = require('md5');
/**
* 支持传入一个html文件的路径渲染出webview
*/
export class WebviewDialog {
public readonly panel: vscode.WebviewPanel;
// public webview(): vscode.Webview { return this.panel.webview; }
opts: any;
hashKey: string;
constructor(
viewType: string,
resourceRootDir: string,
dbPath: string,
webViewPanel?: vscode.WebviewPanel,
iTitle?: string,
icon?: string,
onMessageHandle?: any,
viewColumn?: vscode.ViewColumn,
opts?: any,
) {
this.opts = opts;
this.hashKey = md5(dbPath);
viewColumn = viewColumn || vscode.ViewColumn.One;// vscode.ViewColumn.One
const options: vscode.WebviewOptions | vscode.WebviewPanelOptions = {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(resourceRootDir),
]
};
const dialogHtmlFile = path.join(resourceRootDir, 'index.html');
let html = fs.readFileSync(dialogHtmlFile, { encoding: 'utf8' });
const title = iTitle || this.extractHtmlTitle(html, 'Dialog');
if (webViewPanel) {
this.panel = webViewPanel;
this.panel.webview.options = {
enableScripts: true,
localResourceRoots: [
vscode.Uri.file(resourceRootDir),
]
};
this.panel.title = title;
this.panel.reveal(viewColumn);
// this.panel = viewType;
} else {
this.panel = vscode.window.createWebviewPanel(
viewType,
title,
viewColumn,
options);
}
html = fixResourceReferences(html, resourceRootDir, this.panel.webview);
html = fixCspSourceReferences(html, this.panel.webview);
this.panel.webview.html = html;
this.panel.iconPath = {
light: vscode.Uri.joinPath(vscode.Uri.file(path.resolve(__dirname, '../assets')), 'light', `${icon}.svg`),
dark: vscode.Uri.joinPath(vscode.Uri.file(path.resolve(__dirname, '../assets')), 'dark', `${icon}.svg`)
};
// https://code.visualstudio.com/api/extension-guides/webview#scripts-and-message-passing
this.panel.webview.onDidReceiveMessage((message: API.IMessage) => {
if (onMessageHandle) {
console.log('hashKey:', this.hashKey);
message.hashkey = this.hashKey;
message.dbPath = dbPath;
onMessageHandle(message, this.panel.webview);
}
});
this.commonEventsHandle();
}
private commonEventsHandle(){
eventService.on(events.CHANGE_THEME, ()=>{
this.panel.webview.postMessage(notificationData(events.CHANGE_THEME, { theme: getTheme() }));
});
}
/**
* 解析 <title> 标签.
*/
private extractHtmlTitle(html: string, defaultTitle: string): string {
const titleMatch = /\<title\>([^<]*)\<\/title\>/.exec(html);
const title = (titleMatch && titleMatch[1]) || defaultTitle;
return title;
}
public getPanel(): vscode.WebviewPanel {
return this.panel;
}
public getWebView(): vscode.Webview {
return this.panel.webview;
}
public reveal() {
return this.panel.reveal();
}
public postMessage(message: any) {
this.panel.webview.postMessage(message);
}
public setTitle(title: string) {
this.panel.title = title;
}
public dispose() {
this.panel.dispose();
}
}
```
## /src/server/api.ts
```ts path="/src/server/api.ts"
import * as vscode from 'vscode';
import * as fs from 'fs/promises';
import { get, set } from 'lodash';
import { v1 } from 'uuid';
import { methods } from '../constant';
import dbServer from './dbServer';
import { responseData, notificationData, getTheme, formartUri, openFile } from '../libs/utils';
import { errorCode } from "../errorCode";
const getAllTables = async (message: API.IMessage, webView: vscode.Webview) => {
const dbHash = message.hashkey;
const resultSet: any = await dbServer.runQuery(dbHash!, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name", {}, message.dbPath);
const result: any = [];
if (resultSet.errorCode === 200) {
const tables = resultSet.data;
for (let i = 0; i < tables.length; i++) {
let table: any = tables[i];
const tableName = table.name;
const columns: any = await dbServer.runQuery(dbHash!, `PRAGMA table_info(${tableName})`, {});
result.push({
tableName: tableName,
columns: columns.data
});
}
}
const response: API.ResponseResult = responseData(message.id, { data: result }, true);
webView.postMessage(response);
};
const executeSql = async (message: API.IMessage, webView: vscode.Webview) => {
const dbHash = message.hashkey;
const resultSet: any = await dbServer.runQuery(dbHash!, message.data.sql, {}, message.dbPath);
webView.postMessage(responseData(message.id, resultSet, true));
};
const getUserFavorites = async (message: API.IMessage, webView: vscode.Webview) => {
const favorites: any = await dbServer.sysQuery('SELECT * FROM t_sys_user_favorites ORDER BY priority DESC, updateAt DESC', {});
webView.postMessage(responseData(message.id, {
errorCode: 200,
data: favorites
}, true)
);
};
const addUserFavorite = async (message: API.IMessage, webView: vscode.Webview) => {
const { name, sql } = message.data;
let result;
if (!name || name.trim() === '' || !sql || sql.trim() === '') {
result = { success: false, code: errorCode['required_parameter_missing'] };
}
const fid = v1();
await dbServer.sysQuery(
`INSERT INTO t_sys_user_favorites (fid, name, sqlStr, createAt, updateAt, priority)
VALUES ($fid, $name, $sqlStr, $createAt, $updateAt, $priority)`,
{ $fid: fid, $name: name, $sqlStr: sql, $createAt: new Date().getTime(), $updateAt: new Date().getTime(), $priority: 0 },
);
result = { success: true, data: fid };
webView.postMessage(responseData(message.id, result, true));
};
/**
* 删除一条用户收藏
* @param fid
* @returns
*/
const removeUserFavorite = async (message: API.IMessage, webView: vscode.Webview) => {
const { id } = message.data;
let result;
if (!id || id.trim() === '') {
result = { success: false, code: errorCode['required_parameter_missing'] };
} else {
await dbServer.sysQuery(
`DELETE FROM t_sys_user_favorites WHERE fid = $fid`,
{ $fid: id }
);
result = { success: true };
}
webView.postMessage(responseData(message.id, result, true));
};
/**
* 重命名一条用户收藏
* @param fid
* @param name
* @returns
*/
const renameUserFavorite = async(message: API.IMessage, webView: vscode.Webview) => {
const { id, name } = message.data;
let result;
if (!id || id.trim() === '' || !name || name.trim() === '') {
result = { success: false, code: errorCode['required_parameter_missing'] };
}
await dbServer.sysQuery(
`UPDATE t_sys_user_favorites SET name = $name, updateAt = $updateAt WHERE fid = $fid`,
{ $fid: id, $name: name, $updateAt: new Date().getTime() }
);
result = { success: true };
webView.postMessage(responseData(message.id, result, true));
};
/**
* 排序用户收藏
* @param fidList
* @returns
*/
const sortUserFavorites = async(message: API.IMessage, webView: vscode.Webview) => {
const { fidList } = message.data;
let result: API.ResponseResult;
if (!fidList || fidList.length === 0) {
result = responseData(message.id, { success: false, code: errorCode['required_parameter_missing'] }, false);
}
for (let i = 0; i < fidList.length; i++) {
const fid = fidList[i];
await dbServer.sysQuery(
`UPDATE t_sys_user_favorites SET priority = $priority WHERE fid = $fid`,
{ $fid: fid, $priority: fidList.length - i }
);
}
result = responseData(message.id, { success: true }, true) ;
webView.postMessage(result);
};
const saveToExcel = async (message: any, webView: vscode.Webview) => {
if (message.data) {
const {fileType, fileStr} = message.data;
const dialogOption = {};
if (fileType === 'excel') {
Object.assign(dialogOption, {
filters: {
'excel': ['xlsx']
}
});
} else if (fileType === 'csv') {
Object.assign(dialogOption, {
filters: {
'csv': ['csv']
}
});
} else {
vscode.window.showErrorMessage('Unsupported file types');
return;
}
const fileUri = await vscode.window.showSaveDialog(dialogOption);
const savePath = get(fileUri, 'path');
if (savePath) {
let fpath = formartUri(savePath);
if (fileType === 'csv') {
await fs.writeFile(fpath, fileStr, 'utf8');
} else if (fileType === 'excel') {
const fileBuffer = Buffer.from(fileStr, 'base64');
await fs.writeFile(fpath, fileBuffer);
}
vscode.window.showInformationMessage('File saved successfully', 'View').then((action) => {
if (action === 'View') {
openFile(fpath);
}
});
}
}
webView.postMessage(responseData(message.id || '', {}, true));
};
const copyToClipboard = async (message: any, webView: vscode.Webview)=>{
const { content } = message.data;
let result: API.ResponseResult;
try {
await vscode.env.clipboard.writeText(content);
result = responseData(message.id, { errorCode: 200, data: 'success' }, true);
} catch (error) {
result = responseData(message.id, { errorCode: 500, message: 'Copy failed, please try again later' }, false);
}
webView.postMessage(result);
};
class ApiMap {
private static instance: ApiMap;
private api: Map<string, any> = new Map();
private constructor() {
this.init();
}
public static getInstance(): ApiMap {
if (!ApiMap.instance) {
ApiMap.instance = new ApiMap();
}
return ApiMap.instance;
}
private add(method: string, fun: Function) {
set(this.api, method, fun);
}
private init() {
this.add(methods.GET_ALL_TABLES, getAllTables);
this.add(methods.EXECUTE_SQL, executeSql);
this.add(methods.GET_USER_FAVORITES, getUserFavorites);
this.add(methods.ADD_USER_FAVORITE, addUserFavorite);
this.add(methods.REMOVE_USER_FAVORITE, removeUserFavorite);
this.add(methods.RENAME_USER_FAVORITE, renameUserFavorite);
this.add(methods.SORT_USER_FAVORITES, sortUserFavorites);
this.add(methods.EXPORT_SQL_RESULT, saveToExcel);
this.add(methods.COPY_TO_CLIPBOARD, copyToClipboard);
}
public get(method?: string): Function {
const errFun = () => {
console.log('Function not fond');
};
if (!method) {
return errFun;
}
return get(this.api, method, errFun);
}
}
export const handleMessages = (message: API.IMessage, webView: vscode.Webview) => {
console.log('message:', message);
if (message.request) {
ApiMap.getInstance().get(message.method)(message, webView);
} else if (message.notification) {
// 收到通知
if (message.method === methods.PAGE_STATUS) {
// 页面状态的通知
if (message.data === 'ready') {
webView.postMessage(notificationData(methods.ROUTER, { router: '/', theme: getTheme() }));
}
}
}
};
```
## /src/server/dbServer.ts
```ts path="/src/server/dbServer.ts"
import logOutput from "../libs/log.service";
import { errorCode } from "../errorCode";
import { sysDbDir, createSysDir } from "../libs/utils";
const md5 = require('md5');
const sqlite3 = require('../sqlite3/lib/sqlite3').verbose();
const getDbInstance = async function (path:string) {
return new Promise((resolve, reject)=>{
let sysInstance: any;
sysInstance = new sqlite3.Database(path, (err: any)=>{
if (err) {
logOutput.error(err);
resolve({ success: false });
} else {
resolve({ success: true, db: sysInstance});
}
});
});
};
class DbPool {
private static instance: DbPool;
private dbMap: Map<string, any> = new Map();
public sysDbInstance: any;
private constructor() {
createSysDir();
}
public static getInstance(): DbPool {
if (!DbPool.instance) {
DbPool.instance = new DbPool();
}
return DbPool.instance;
}
private closeSysDb(){
if(this.sysDbInstance) {
this.sysDbInstance.close();
this.sysDbInstance = null;
}
}
private async initSysDb() {
if(!this.sysDbInstance) {
const instanceResult: any = await getDbInstance(sysDbDir());
if(instanceResult.success) {
this.sysDbInstance = instanceResult.db;
this.addDefaultTables();
}
}
}
private async addDefaultTables() {
/**
* 创建用户收藏表
* 收藏的SQL语句
* priority: 优先级,数字越大优先级越高
*/
this.sysDbInstance.run(`
CREATE TABLE IF NOT EXISTS
t_sys_user_favorites(
fid varchar(38) primary key,
name text,
sqlStr text,
createAt varchar(20),
updateAt varchar(20),
priority int)
`);
}
public async sysQuery(sqlStr: string, paramArray: any) {
console.log('Query SQL:', sqlStr);
console.log('Query param:', paramArray);
if(!this.sysDbInstance) {
await this.initSysDb();
}
return new Promise((resolve, reject) => {
this.sysDbInstance.serialize(async () => {
this.sysDbInstance.all(sqlStr, paramArray, function (err: any, rows: any) {
// console.log('Query rows:', rows)
if (err) {
logOutput.error(err);
console.error('Query Error:', err);
resolve([]);
return;
}
resolve(rows);
});
});
});
}
public async getDb(dbHash: string, path?: string) {
if (this.dbMap.has(dbHash)) {
return this.dbMap.get(dbHash).db;
} else {
if (!path) {
return null;
}
const db = await this.addDb(path);
return db;
}
}
public async addDb(path: string) {
const dbHash = md5(path);
if (this.dbMap.has(dbHash)) {
return this.dbMap.get(dbHash).db;
}
const getInstanceResult: any = await getDbInstance(path);
if (!getInstanceResult.success) {
return null;
}
this.dbMap.set(dbHash, { db: getInstanceResult.db, path});
return getInstanceResult.db;
}
public closeDb(dbHash: string) {
if (this.dbMap.has(dbHash)) {
const db = this.dbMap.get(dbHash);
db.close();
this.dbMap.delete(dbHash);
}
}
public closeAllDbs() {
this.dbMap.forEach((dbInfo, dbHash) => {
dbInfo.db.close();
this.dbMap.delete(dbHash);
});
}
public async runQuery(dbHash: string, sql: string, params: any = {}, path?: string): Promise<any> {
return new Promise(async (resolve, reject) => {
const db: any = await this.getDb(dbHash, path);
if (!db) {
logOutput.error('Database not opened:', dbHash);
resolve({ success: false, errorCode: errorCode.database_not_opened });
return;
}
db.all(sql, params, (err: any, rows: any[]) => {
if (err) {
logOutput.error(err);
resolve({ errorCode: errorCode.db_quey_error, data: [], error: err });
} else {
resolve({ errorCode: 200, data: rows});
}
});
});
}
}
export default DbPool.getInstance();
```
## /src/sqlite3/lib/bindings.js
```js path="/src/sqlite3/lib/bindings.js"
/**
* Module dependencies.
*/
var fs = require('fs'),
path = require('path'),
fileURLToPath = require('file-uri-to-path'),
join = path.join,
dirname = path.dirname,
exists =
(fs.accessSync &&
function(path) {
try {
fs.accessSync(path);
} catch (e) {
return false;
}
return true;
}) ||
fs.existsSync ||
path.existsSync,
defaults = {
arrow: process.env.NODE_BINDINGS_ARROW || ' → ',
compiled: process.env.NODE_BINDINGS_COMPILED_DIR || 'compiled',
platform: process.platform,
arch: process.arch,
nodePreGyp:
'node-v' +
process.versions.modules +
'-' +
process.platform +
'-' +
process.arch,
version: process.versions.node,
bindings: 'bindings.node',
try: [
// node-gyp's linked version in the "build" dir
['module_root', 'build', 'bindings'],
// node-waf and gyp_addon (a.k.a node-gyp)
['module_root', 'build', 'Debug', 'bindings'],
['module_root', 'build', 'Release', 'bindings'],
// Debug files, for development (legacy behavior, remove for node v0.9)
['module_root', 'out', 'Debug', 'bindings'],
['module_root', 'Debug', 'bindings'],
// Release files, but manually compiled (legacy behavior, remove for node v0.9)
['module_root', 'out', 'Release', 'bindings'],
['module_root', 'Release', 'bindings'],
// Legacy from node-waf, node <= 0.4.x
['module_root', 'build', 'default', 'bindings'],
// Production "Release" buildtype binary (meh...)
['module_root', 'compiled', 'version', 'platform', 'arch', 'bindings'],
// node-qbs builds
['module_root', 'addon-build', 'release', 'install-root', 'bindings'],
['module_root', 'addon-build', 'debug', 'install-root', 'bindings'],
['module_root', 'addon-build', 'default', 'install-root', 'bindings'],
// node-pre-gyp path ./lib/binding/{node_abi}-{platform}-{arch}
['module_root', 'lib', 'binding', 'nodePreGyp', 'bindings']
]
};
/**
* The main `bindings()` function loads the compiled bindings for a given module.
* It uses V8's Error API to determine the parent filename that this function is
* being invoked from, which is then used to find the root directory.
*/
function bindings(opts) {
// Argument surgery
if (typeof opts == 'string') {
opts = { bindings: opts };
} else if (!opts) {
opts = {};
}
// maps `defaults` onto `opts` object
Object.keys(defaults).map(function(i) {
if (!(i in opts)) opts[i] = defaults[i];
});
// Get the module root
if (!opts.module_root) {
opts.module_root = exports.getRoot(exports.getFileName());
}
// Ensure the given bindings name ends with .node
if (path.extname(opts.bindings) != '.node') {
opts.bindings += '.node';
}
// https://github.com/webpack/webpack/issues/4175#issuecomment-342931035
var requireFunc =
typeof __webpack_require__ === 'function'
? __non_webpack_require__
: require;
var tries = [],
i = 0,
l = opts.try.length,
n,
b,
err;
for (; i < l; i++) {
n = join.apply(
null,
opts.try[i].map(function(p) {
return opts[p] || p;
})
);
tries.push(n);
try {
b = opts.path ? requireFunc.resolve(n) : requireFunc(n);
if (!opts.path) {
b.path = n;
}
return b;
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND' &&
e.code !== 'QUALIFIED_PATH_RESOLUTION_FAILED' &&
!/not find/i.test(e.message)) {
throw e;
}
}
}
err = new Error(
'Could not locate the bindings file. Tried:\n' +
tries
.map(function(a) {
return opts.arrow + a;
})
.join('\n')
);
err.tries = tries;
throw err;
}
module.exports = exports = bindings;
/**
* Gets the filename of the JavaScript file that invokes this function.
* Used to help find the root directory of a module.
* Optionally accepts an filename argument to skip when searching for the invoking filename
*/
exports.getFileName = function getFileName(calling_file) {
var origPST = Error.prepareStackTrace,
origSTL = Error.stackTraceLimit,
dummy = {},
fileName;
Error.stackTraceLimit = 10;
Error.prepareStackTrace = function(e, st) {
for (var i = 0, l = st.length; i < l; i++) {
fileName = st[i].getFileName();
if (fileName !== __filename) {
if (calling_file) {
if (fileName !== calling_file) {
return;
}
} else {
return;
}
}
}
};
// run the 'prepareStackTrace' function above
Error.captureStackTrace(dummy);
dummy.stack;
// cleanup
Error.prepareStackTrace = origPST;
Error.stackTraceLimit = origSTL;
// handle filename that starts with "file://"
var fileSchema = 'file://';
if(!fileName){
return __dirname;
}
if (fileName.indexOf(fileSchema) === 0) {
fileName = fileURLToPath(fileName);
}
return fileName;
};
/**
* Gets the root directory of a module, given an arbitrary filename
* somewhere in the module tree. The "root directory" is the directory
* containing the `package.json` file.
*
* In: /home/nate/node-native-module/lib/index.js
* Out: /home/nate/node-native-module
*/
exports.getRoot = function getRoot(file) {
var dir = dirname(file),
prev;
while (true) {
if (dir === '.') {
// Avoids an infinite loop in rare cases, like the REPL
dir = process.cwd();
}
if (
exists(join(dir, 'package.json')) ||
exists(join(dir, 'node_modules'))
) {
// Found the 'package.json' file or 'node_modules' dir; we're done
return dir;
}
if (prev === dir) {
// Got to the top
throw new Error(
'Could not find module root given file: "' +
file +
'". Do you have a `package.json` file? '
);
}
// Try the parent dir next
prev = dir;
dir = join(dir, '..');
}
};
```
## /src/sqlite3/lib/sqlite3-binding.js
```js path="/src/sqlite3/lib/sqlite3-binding.js"
const path = require('path');
const os = require('os');
console.log('DB file path is:', path.resolve(__dirname, `../build/${os.type()}/${os.arch()}/node_sqlite3.node`))
module.exports = require('./bindings')(`${os.type()}/${os.arch()}/node_sqlite3.node`);
```
## /src/sqlite3/lib/sqlite3.js
```js path="/src/sqlite3/lib/sqlite3.js"
const path = require('path');
const sqlite3 = require('./sqlite3-binding.js');
const EventEmitter = require('events').EventEmitter;
module.exports = exports = sqlite3;
function normalizeMethod (fn) {
return function (sql) {
let errBack;
const args = Array.prototype.slice.call(arguments, 1);
if (typeof args[args.length - 1] === 'function') {
const callback = args[args.length - 1];
errBack = function(err) {
if (err) {
callback(err);
}
};
}
const statement = new Statement(this, sql, errBack);
return fn.call(this, statement, args);
};
}
function inherits(target, source) {
for (const k in source.prototype)
target.prototype[k] = source.prototype[k];
}
sqlite3.cached = {
Database: function(file, a, b) {
if (file === '' || file === ':memory:') {
// Don't cache special databases.
return new Database(file, a, b);
}
let db;
file = path.resolve(file);
if (!sqlite3.cached.objects[file]) {
db = sqlite3.cached.objects[file] = new Database(file, a, b);
}
else {
// Make sure the callback is called.
db = sqlite3.cached.objects[file];
const callback = (typeof a === 'number') ? b : a;
if (typeof callback === 'function') {
function cb() { callback.call(db, null); }
if (db.open) process.nextTick(cb);
else db.once('open', cb);
}
}
return db;
},
objects: {}
};
const Database = sqlite3.Database;
const Statement = sqlite3.Statement;
const Backup = sqlite3.Backup;
inherits(Database, EventEmitter);
inherits(Statement, EventEmitter);
inherits(Backup, EventEmitter);
// Database#prepare(sql, [bind1, bind2, ...], [callback])
Database.prototype.prepare = normalizeMethod(function(statement, params) {
return params.length
? statement.bind.apply(statement, params)
: statement;
});
// Database#run(sql, [bind1, bind2, ...], [callback])
Database.prototype.run = normalizeMethod(function(statement, params) {
statement.run.apply(statement, params).finalize();
return this;
});
// Database#get(sql, [bind1, bind2, ...], [callback])
Database.prototype.get = normalizeMethod(function(statement, params) {
statement.get.apply(statement, params).finalize();
return this;
});
// Database#all(sql, [bind1, bind2, ...], [callback])
Database.prototype.all = normalizeMethod(function(statement, params) {
statement.all.apply(statement, params).finalize();
return this;
});
// Database#each(sql, [bind1, bind2, ...], [callback], [complete])
Database.prototype.each = normalizeMethod(function(statement, params) {
statement.each.apply(statement, params).finalize();
return this;
});
Database.prototype.map = normalizeMethod(function(statement, params) {
statement.map.apply(statement, params).finalize();
return this;
});
// Database#backup(filename, [callback])
// Database#backup(filename, destName, sourceName, filenameIsDest, [callback])
Database.prototype.backup = function() {
let backup;
if (arguments.length <= 2) {
// By default, we write the main database out to the main database of the named file.
// This is the most likely use of the backup api.
backup = new Backup(this, arguments[0], 'main', 'main', true, arguments[1]);
} else {
// Otherwise, give the user full control over the sqlite3_backup_init arguments.
backup = new Backup(this, arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
}
// Per the sqlite docs, exclude the following errors as non-fatal by default.
backup.retryErrors = [sqlite3.BUSY, sqlite3.LOCKED];
return backup;
};
Statement.prototype.map = function() {
const params = Array.prototype.slice.call(arguments);
const callback = params.pop();
params.push(function(err, rows) {
if (err) return callback(err);
const result = {};
if (rows.length) {
const keys = Object.keys(rows[0]);
const key = keys[0];
if (keys.length > 2) {
// Value is an object
for (let i = 0; i < rows.length; i++) {
result[rows[i][key]] = rows[i];
}
} else {
const value = keys[1];
// Value is a plain value
for (let i = 0; i < rows.length; i++) {
result[rows[i][key]] = rows[i][value];
}
}
}
callback(err, result);
});
return this.all.apply(this, params);
};
let isVerbose = false;
const supportedEvents = [ 'trace', 'profile', 'change' ];
Database.prototype.addListener = Database.prototype.on = function(type) {
const val = EventEmitter.prototype.addListener.apply(this, arguments);
if (supportedEvents.indexOf(type) >= 0) {
this.configure(type, true);
}
return val;
};
Database.prototype.removeListener = function(type) {
const val = EventEmitter.prototype.removeListener.apply(this, arguments);
if (supportedEvents.indexOf(type) >= 0 && !this._events[type]) {
this.configure(type, false);
}
return val;
};
Database.prototype.removeAllListeners = function(type) {
const val = EventEmitter.prototype.removeAllListeners.apply(this, arguments);
if (supportedEvents.indexOf(type) >= 0) {
this.configure(type, false);
}
return val;
};
// Save the stack trace over EIO callbacks.
sqlite3.verbose = function() {
if (!isVerbose) {
const trace = require('./trace');
[
'prepare',
'get',
'run',
'all',
'each',
'map',
'close',
'exec'
].forEach(function (name) {
trace.extendTrace(Database.prototype, name);
});
[
'bind',
'get',
'run',
'all',
'each',
'map',
'reset',
'finalize',
].forEach(function (name) {
trace.extendTrace(Statement.prototype, name);
});
isVerbose = true;
}
return sqlite3;
};
```
## /src/sqlite3/lib/trace.js
```js path="/src/sqlite3/lib/trace.js"
// Inspired by https://github.com/tlrobinson/long-stack-traces
const util = require('util');
function extendTrace(object, property, pos) {
const old = object[property];
object[property] = function() {
const error = new Error();
const name = object.constructor.name + '#' + property + '(' +
Array.prototype.slice.call(arguments).map(function(el) {
return util.inspect(el, false, 0);
}).join(', ') + ')';
if (typeof pos === 'undefined') pos = -1;
if (pos < 0) pos += arguments.length;
const cb = arguments[pos];
if (typeof arguments[pos] === 'function') {
arguments[pos] = function replacement() {
const err = arguments[0];
if (err && err.stack && !err.__augmented) {
err.stack = filter(err).join('\n');
err.stack += '\n--> in ' + name;
err.stack += '\n' + filter(error).slice(1).join('\n');
err.__augmented = true;
}
return cb.apply(this, arguments);
};
}
return old.apply(this, arguments);
};
}
exports.extendTrace = extendTrace;
function filter(error) {
return error.stack.split('\n').filter(function(line) {
return line.indexOf(__filename) < 0;
});
}
```
## /src/test/extension.test.ts
```ts path="/src/test/extension.test.ts"
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));
});
});
```
## /tsconfig.json
```json path="/tsconfig.json"
{
"compilerOptions": {
"module": "Node16",
"target": "ES2022",
"lib": [
"ES2022"
],
"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. */
}
}
```
## /ui/README.md
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
## /ui/package.json
```json path="/ui/package.json"
{
"name": "ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@reduxjs/toolkit": "^1.8.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@visactor/vtable": "^1.19.7",
"@visactor/vtable-export": "^1.19.7",
"antd": "^5.27.3",
"buffer": "^6.0.3",
"monaco-editor": "^0.53.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4",
"react-redux": "^8.0.1",
"react-router": "^7.1.5",
"react-router-dom": "^7.1.1",
"react-sticky-box": "^2.0.5",
"sql-formatter": "^15.6.6"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
```
## /ui/public/favicon.ico
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/ui/public/favicon.ico
## /ui/public/index.html
```html path="/ui/public/index.html"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Free Sqlite</title>
<style>
.loadingwarp {
width: 100%;
height: calc(100vh - 48px);
display: flex;
justify-content: center;
align-items: center;
}
.nullloading {
width: 30px;
height: 30px;
border: 2px solid #1677ff;
border-top-color: transparent;
border-radius: 100%;
animation: circle infinite 0.75s linear;
}
@keyframes circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div class="loadingwarp">
<div class="nullloading"></div>
</div>
</div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
```
## /ui/public/logo192.png
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/ui/public/logo192.png
## /ui/public/logo512.png
Binary file available at https://raw.githubusercontent.com/fjb040911/free-sqlite/refs/heads/main/ui/public/logo512.png
## /ui/public/manifest.json
```json path="/ui/public/manifest.json"
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
```
## /ui/public/robots.txt
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
## /ui/src/App.css
```css path="/ui/src/App.css"
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.app-container {
padding: 16px 16px 0 16px;
}
.card-header {
top: 0px;
position: sticky;
/* background-color: transparent; */
backdrop-filter: blur(6px);
z-index: 950;
/* border-bottom: 1px solid rgba(5, 5, 5, 0.06); */
grid-area: head;
align-self: stretch;
padding: 8px 16px !important;
}
.card-body {
min-height: 100vh;
}
.card-body-with-header {
min-height: calc(100vh - 56px);
}
.ant-form-item .ant-form-item-label >label {
height: 100% !important;
}
#root {
height: 100vh;
}
```
## /ui/src/App.js
```js path="/ui/src/App.js"
import React, { useEffect } from 'react';
import { useDispatch } from "react-redux";
import { Routes, Route, useNavigate } from "react-router";
import Layout from './Layout';
import { handelResponse, postMessage, formatNotification } from "./stores/rpcRequest";
import { setVsTheme } from "./stores/homeSlice";
import { devMode } from './constant';
import SqlEditor from './features/sqlEditor';
import './App.css';
function App() {
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
function handleEvent() {
window.addEventListener("message", (event) => {
// console.log("event message:", event);
const message = devMode ? event.detail : event.data;
console.log("app message:", message);
if (!message) {
return;
}
if (message.notification) {
// 通知消息
// 路由消息
if (message.method === "ROUTER") {
const { router, theme } = message.data;
console.log('router---', router);
console.log('theme---', theme);
router && navigate(router);
dispatch(setVsTheme(theme));
} else if (message.method === "CHANGE_THEME") {
const { theme } = message.data;
dispatch(setVsTheme(theme));
}
}
if (message.response) {
// API交互消息
handelResponse(message);
}
});
}
handleEvent();
postMessage(formatNotification("PAGE_STATUS", "ready"));
}, []);
return (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<SqlEditor />} />
</Route>
</Routes>
);
}
export default App;
```
## /ui/src/App.test.js
```js path="/ui/src/App.test.js"
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
```
## /ui/src/Layout.jsx
```jsx path="/ui/src/Layout.jsx"
import React from "react";
import { useSelector} from "react-redux";
import { Outlet } from "react-router-dom";
import { ConfigProvider, theme } from 'antd';
import { vsTheme } from "./stores/homeSlice";
function Layout(props) {
const iVsTheme = useSelector(vsTheme);
return (
<ConfigProvider
theme={{
algorithm: iVsTheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
}}
>
<Outlet/>
</ConfigProvider>
);
}
export default Layout;
```
## /ui/src/constant.js
```js path="/ui/src/constant.js"
// 是否为开发模式
export const devMode = false;
```
## /ui/src/features/sqlEditor/Editor/SqlEditor.jsx
```jsx path="/ui/src/features/sqlEditor/Editor/SqlEditor.jsx"
import React, {
useImperativeHandle,
useEffect,
useRef,
useState,
forwardRef,
} from "react";
import * as monaco from "monaco-editor";
import { editor, languages } from "monaco-editor";
import { debounce } from "lodash";
// 导入 codicon 字体
import codicon from "monaco-editor/esm/vs/base/browser/ui/codicons/codicon//codicon.ttf";
import { language as sqlLanguage } from "monaco-editor/esm/vs/basic-languages/sql/sql.js";
import {
sqlAutoComplete,
getTableSuggest,
getClomnSuggest,
formatSQL,
} from "./utils";
const { keywords } = sqlLanguage;
const defaultOptions = {
selectOnLineNumbers: true,
roundedSelection: false,
cursorStyle: "line",
readOnly: false,
fontSize: 16,
minimap: {
// 小地图配置
enabled: false, // 禁用小地图
},
glyphMargin: true,
folding:false,
automaticLayout: true, // 自动布局
scrollBeyondLastLine: false, // 不允许滚动超过最后一行
glyphMargin: false,
};
const languageDef = "sql"; // 语言,默认 SQL
// 加载字体的异步函数
export async function loadFont(fontFamily, url) {
const font = new FontFace(fontFamily, `local(${fontFamily}), url(${url})`);
await font.load();
document.fonts.add(font);
}
export const SqlEditor = forwardRef((props, ref)=> {
const {
theme = "naruto", // 主题
autoComplete = sqlAutoComplete, // 默认sql自动补全
options = {}, // 编辑器选项
dataBase = [], //数据库表数据
} = props;
const mergedOptions = {
...defaultOptions,
...options,
};
const editorContainerRef = useRef(null);
const $editor = useRef();// 编辑器引用
// const [monacoEditor, setMonacoEditor] = useState(null);
const monacoEditor = monaco.editor
useEffect(() => {
editor.setTheme(`vs-${theme}`); // 设置主题
}, [theme]);
// 初始化编辑器
useEffect(() => {
console.log('monacoEditor:', monacoEditor)
console.log('editorContainerRef.current:', editorContainerRef.current)
if (monacoEditor && editorContainerRef.current) {
// 加载 codicon 字体
loadFont("codicon", codicon);
// 注册 SQL 语言
monaco.languages.register({ id: languageDef });
monaco.languages.setMonarchTokensProvider(languageDef, sqlLanguage);
// 创建编辑器实例
$editor.current = monacoEditor.create(editorContainerRef.current, {
language: languageDef,
...mergedOptions
});
// 设置语言为 SQL
}
}, []);
//动态引入
// useEffect(() => {
// console.log('----------useEffect------------', monaco.editor)
// const loadMonaco = async () => {
// const monacoModule = (await import("monaco-editor")).editor;
// setMonacoEditor(monacoModule);
// };
// loadMonaco();
// }, []);
// 定义 format 方法
const format = () => {
if ($editor.current) {
$editor.current.setValue(formatSQL($editor.current.getValue()));
}
};
// 暴露引用给父组件
useImperativeHandle(ref, () => ({
editor: $editor.current,
monaco,
format,
}));
// 注册自动补全提供者
useEffect(() => {
let CPDisposable;
if ($editor.current && autoComplete) {
const _model = $editor.current.getModel();
const _position = $editor.current.getPosition();
if (_model && _position) {
CPDisposable = languages.registerCompletionItemProvider(languageDef, {
triggerCharacters: [".", ...keywords],
provideCompletionItems: (model, position) => {
let suggestions = [];
const { lineNumber, column } = position;
/* 获取当前光标所在行的文本 */
const beforeEditingText = model.getValueInRange({
startLineNumber: lineNumber,
startColumn: 0,
endLineNumber: lineNumber,
endColumn: column,
});
/* 正在编辑的单词 */
const tokens = beforeEditingText.trim().split(/\s+/);
const editingWord = tokens[tokens.length - 1];
/* .结尾 */
if (editingWord?.endsWith(".")) {
const wordNoDot = editingWord.slice(0, editingWord.length - 1);
suggestions = [...getClomnSuggest(dataBase, wordNoDot)];
} else if (editingWord === ".") {
/* .开头 */
suggestions = [];
} else {
suggestions = [
...getTableSuggest(dataBase),
...autoComplete(model, position),
];
}
return {
suggestions,
};
},
});
}
}
// 组件卸载时销毁自动补全提供者
return () => {
CPDisposable && CPDisposable.dispose();
};
}, [$editor.current, autoComplete, dataBase]);
// 组件卸载时销毁编辑器实例
useEffect(() => {
resizeObserver.observe(editorContainerRef.current);
return () => {
if ($editor.current) {
$editor.current.dispose();
}
};
}, []);
const resizeObserver = new ResizeObserver(([entry] = []) => {
const [size] = entry.borderBoxSize || [];
debounceRender(size.inlineSize, size.blockSize);
});
const debounceRender = debounce(async (width, height) => {
if ($editor.current) {
$editor.current.layout();
}
}, 200);
return (
<div
ref={editorContainerRef}
id='tests4'
style={{
width: "100%",
height: "100%" ,
}}
>
</div>
);
})
```
## /ui/src/features/sqlEditor/Editor/index.jsx
```jsx path="/ui/src/features/sqlEditor/Editor/index.jsx"
import { useRef, useImperativeHandle, forwardRef } from "react";
import { useDispatch, useSelector } from 'react-redux';
import { Card, message, Space } from 'antd';
import { tableTree, executeSqlAsync } from '../../../stores/sqlSlice';
import { vsTheme } from '../../../stores/homeSlice';
import { SqlEditor } from "./SqlEditor";
const Editor = forwardRef((props, ref) => {
const dispatch = useDispatch();
const [messageApi, contextHolder] = message.useMessage();
const editorRef = useRef(null);
const iTableTree = useSelector(tableTree);
const iVsTheme = useSelector(vsTheme);
const handleExecuteSql = async () => {
console.log('editorRef.current:', editorRef.current);
const currentSql = editorRef.current?.editor?.getValue() || '';
console.log('currentSql:', currentSql);
if (currentSql) {
const result = await dispatch(executeSqlAsync({ sql: currentSql }));
// console.log('SQL 执行结果:', result);
if (result.payload.errorCode !== 200) {
messageApi.error('SQL语句执行失败, 请检查SQL语句是否正确');
}
}
}
// 暴露引用给父组件
useImperativeHandle(ref, () => ({
editor: editorRef.current,
format: () => editorRef.current?.format(),
getValue: () => editorRef.current?.editor?.getValue(),
setValue: (value) => editorRef.current?.editor?.setValue(value),
executeSql: handleExecuteSql
}));
return (
<Card
variant="borderless"
size="small"
extra={<Space>{props.toolBarExtra}</Space>}
style={{ height: '100%', boxShadow: 'none' }}
styles={{
body: { padding: '8px 0 0 0', height: 'calc(100% - 38px)' },
}}
>
<SqlEditor style={{ height: '100%' }} theme={iVsTheme} dataBase={iTableTree} ref={editorRef} />
{contextHolder}
</Card>
);
});
export default Editor;
```
## /ui/src/features/sqlEditor/Editor/utils.js
```js path="/ui/src/features/sqlEditor/Editor/utils.js"
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
import { find, uniq } from "lodash";
import { language as sqlLanguage } from 'monaco-editor/esm/vs/basic-languages/sql/sql.js';
import { format } from 'sql-formatter'
const { keywords } = sqlLanguage
console.log('sqlAutoComplete keywords', keywords)
// sql自动补全关键字
export const sqlAutoComplete = () => {
const suggestions = keywords.map((item) => ({
label: item,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: `${item} `,
detail: 'DB Keyword',
}))
return suggestions;
}
// 数据库名
export const getDBSuggest = (dataBase) => {
const suggestions = Object.keys(dataBase).map((key) => ({
label: key,
kind: monaco.languages.CompletionItemKind.Constant,
insertText: key,
detail: '数据库',
}))
return suggestions;
}
export const getClomnSuggest = (hintData=[], tableName) => {
if (!tableName) {
return []
}
const table = find(hintData, tableInfo => tableInfo.tableName === tableName)
if (table && Array.isArray(table.columns)) {
// 有精确找到表
// 返回表字段
return table.columns.map((col) => ({
label: col.name,
kind: monaco.languages.CompletionItemKind.Field,
insertText: col.name,
detail: 'Column',
}))
} else {
// 没有精确找到表
// 返回所有表的字段
let allColumns = []
hintData.forEach(tableInfo => {
if (Array.isArray(tableInfo.columns)) {
allColumns = allColumns.concat(tableInfo.columns)
}
})
let columnNames = allColumns.map(col => col.name)
columnNames = uniq(columnNames) // 去重
return columnNames.map((name) => ({
label: name,
kind: monaco.languages.CompletionItemKind.Field,
insertText: name,
detail: 'Column',
}))
}
}
// 获取表名
export const getTableSuggest = (hintData, dbName) => {
const tableNames = hintData.map(table => table.tableName)
return tableNames.map((name) => ({
label: name,
kind: monaco.languages.CompletionItemKind.Constant,
insertText: name,
detail: 'Table',
}))
}
// 格式化 SQL
export const formatSQL = (sql) => {
return format(sql)
}
// 初始化 函数
export function noop() {}
export function processSize(size) {
return /^\d+$/.test(size) ? `${size}px` : size;
}
```
## /ui/src/features/sqlEditor/Favorite/AddOrEditModal.jsx
```jsx path="/ui/src/features/sqlEditor/Favorite/AddOrEditModal.jsx"
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { Modal, Form, Input, message } from 'antd';
import { get } from 'lodash';
import { addUserFavoriteAsync, renameUserFavoriteAsync, getUserFavoritesAsync } from '../../../stores/sqlSlice';
const AddOrEditModal = props => {
const dispatch = useDispatch();
const [form] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
const { isOpen, sql, favorite, onClose } = props;
useEffect(() => {
if (favorite) {
form.setFieldsValue({ name: favorite.name });
}
}, [favorite, form]);
const onCreate = async (values) => {
console.log('Received values from form: ', values);
if (favorite) {
// 编辑
const res = await dispatch(renameUserFavoriteAsync({ id: favorite.fid, name: values.name }));
if (get(res, 'payload.success')) {
messageApi.open({
type: 'success',
content: '重命名成功',
});
onClose();
form.resetFields();
dispatch(getUserFavoritesAsync());
} else {
messageApi.open({
type: 'error',
content: '重命名失败,请稍后重试',
});
}
} else {
// 新增
const res = await dispatch(addUserFavoriteAsync({ name: values.name, sql }));
if (get(res, 'payload.success')) {
messageApi.open({
type: 'success',
content: '加入成功',
});
onClose();
form.resetFields();
dispatch(getUserFavoritesAsync());
} else {
messageApi.open({
type: 'error',
content: '加入失败,请稍后重试',
});
}
}
}
console.log('isOpen:', isOpen)
return (
<Modal
title={favorite ? '重命名' : '加入收藏夹'}
open={isOpen}
onCancel={() => {
onClose();
form.resetFields();
}}
okButtonProps={{ autoFocus: true, htmlType: 'submit' }}
modalRender={dom => (
<Form
name="addOrEditForm"
form={form}
labelCol={{ span: 4 }}
clearOnDestroy
onFinish={values => onCreate(values)}
>
{dom}
</Form>
)}
>
<Form.Item label="名称" name='name'>
<Input showCount maxLength={15} />
</Form.Item>
{contextHolder}
</Modal>
);
};
export default AddOrEditModal;
```
## /ui/src/features/sqlEditor/Favorite/index.jsx
```jsx path="/ui/src/features/sqlEditor/Favorite/index.jsx"
import { useEffect, useState, useImperativeHandle, forwardRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { DndContext, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { get } from 'lodash';
import { Card, Button, message, Dropdown, Tooltip, Table, Space } from 'antd';
import { FormOutlined, DeleteOutlined, PlayCircleOutlined, EllipsisOutlined, StarOutlined, ArrowLeftOutlined, ReloadOutlined } from '@ant-design/icons';
import { favoriteList, favoriteListStatus, getUserFavoritesAsync, removeUserFavoriteAsync, executeSqlAsync, sortUserFavoritesAsync, setFavoriteList } from '../../../stores/sqlSlice';
import AddOrEditModal from './AddOrEditModal';
const Row = props => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: props['data-row-key'],
});
const style = {
...props.style,
transform: CSS.Translate.toString(transform),
transition,
cursor: 'move',
...(isDragging ? { position: 'relative', zIndex: 999 } : {}),
};
return <tr {...props} ref={setNodeRef} style={style} {...attributes} {...listeners} />;
};
const Favorite = forwardRef((props, ref) => {
const dispatch = useDispatch();
const [messageApi, contextHolder] = message.useMessage();
const { editorRef } = props;
const iFavoriteList = useSelector(favoriteList);
const iFavoriteListStatus = useSelector(favoriteListStatus);
const [favoriteOpen, setFavoriteOpen] = useState(false);
const [currentFavorite, setCurrentFavorite] = useState(null);
useEffect(() => {
dispatch(getUserFavoritesAsync());
}, [dispatch]);
const handleAddFavorite = () => {
console.log('handleAddFavorite currentSql:', editorRef.current.getValue());
if (!editorRef.current?.getValue()) {
messageApi.warning('Please enter SQL statement');
return;
}
setFavoriteOpen(true);
}
const handleRemoveFavorite = async (id) => {
const removeResult = await dispatch(removeUserFavoriteAsync({ id }));
// console.log('删除收藏结果:', removeResult);
if (removeResult.payload.success) {
messageApi.success('Deleted successfully');
dispatch(getUserFavoritesAsync());
} else {
messageApi.error('Delete failed');
}
};
const handleExecuteSql = async (sql) => {
if (sql) {
const result = await dispatch(executeSqlAsync({ sql }));
// console.log('SQL 执行结果:', result);
if (result.payload.errorCode !== 200) {
messageApi.error('SQL execution failed, please check if the SQL statement is correct');
}
}
}
useImperativeHandle(ref, () => ({
handleAddFavorite: handleAddFavorite,
}));
const handleButtonClick = (item, key) => {
console.log('click', item, key);
if (key === 'delete') {
handleRemoveFavorite(item.fid);
} else if (key === 'edit') {
setCurrentFavorite(item);
setFavoriteOpen(true);
} else if (key === 'fill') {
editorRef.current.setValue(item.sqlStr);
editorRef.current.format();
}
}
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
distance: 1,
},
}),
);
const onDragEnd = async (e) => {
const { active, over } = e;
if (active.id !== over?.id) {
const activeIndex = iFavoriteList.findIndex(i => i.fid === active.id);
const overIndex = iFavoriteList.findIndex(i => i.fid === over?.id);
const newList = arrayMove(iFavoriteList, activeIndex, overIndex);
await dispatch(setFavoriteList(newList));
const fidList = get(active, 'data.current.sortable.items', []);
dispatch(sortUserFavoritesAsync({ fidList }));
}
};
const columns = [
{
title: 'Name',
dataIndex: 'name',
ellipsis: true,
},
{
title: 'Actions',
width: 50,
render: (_, record) => {
return (
<Space>
<Tooltip title="Run Query">
<a key='play' onClick={() => handleExecuteSql(record.sqlStr)}><PlayCircleOutlined /></a>
</Tooltip>
<Dropdown
menu={{
items: [
{
key: 'fill',
label: 'View SQL',
icon: <ArrowLeftOutlined />
},
{
key: 'edit',
icon: <FormOutlined />,
label: 'Rename'
},
{
key: 'delete',
danger: true,
icon: <DeleteOutlined />,
label: 'Delete'
}
],
onClick: ({ key }) => handleButtonClick(record, key)
}}
>
<a key='more'><EllipsisOutlined /></a>
</Dropdown>
</Space>
);
}
}
];
return (
<>
<Card
variant="borderless"
size="small"
style={{ height: '100%', boxShadow: 'none' }}
styles={{
body: { height: 'calc(100% - 38px)', overflow: 'auto', padding: '1px 0 0 0' },
}}
title={<i style={{ fontStyle: 'normal' }}><StarOutlined /> Favorites</i>}
extra={<Button color="primary" variant="text" size="small" icon={<ReloadOutlined />} onClick={() => dispatch(getUserFavoritesAsync())} />}
>
<DndContext sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>
<SortableContext
// rowKey array
items={iFavoriteList.map(i => i.fid)}
strategy={verticalListSortingStrategy}
>
<Table
showHeader={false}
size='small'
components={{
body: { row: Row },
}}
rowKey="fid"
columns={columns}
loading={iFavoriteListStatus === 'loading'}
dataSource={iFavoriteList}
pagination={false}
/>
</SortableContext>
</DndContext>
</Card>
<AddOrEditModal
isOpen={favoriteOpen}
sql={editorRef.current?.getValue()}
favorite={currentFavorite}
onClose={() => { setFavoriteOpen(false); setCurrentFavorite(null); }}
/>
{contextHolder}
</>
);
});
export default Favorite;
```
## /ui/src/features/sqlEditor/QueryResult/index.jsx
```jsx path="/ui/src/features/sqlEditor/QueryResult/index.jsx"
import { useMemo, useRef, useEffect, useImperativeHandle, forwardRef } from 'react';
import { Empty, Spin } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import { ListTable, themes } from '@visactor/vtable';
import { exportVTableToCsv, exportVTableToExcel } from '@visactor/vtable-export';
import { Buffer } from 'buffer';
import { vsTheme, writeToClipboardAsync } from "../../../stores/homeSlice";
import { sqlResult, executeSqlStatus, exportToSaveAsync } from '../../../stores/sqlSlice';
import { getVScodeColor } from '../../../utils';
function getBackgroundColor(args) {
const { row, table } = args;
const index = row - table.frozenRowCount;
if (!(index & 1)) {
return getVScodeColor('--vscode-editorWidget-background')||'#252526';
}
return getVScodeColor('--x-thead-background')||'#1e1e1e';
}
const getThemes = (theme) => {
if(theme === 'dark') {
const hoverBgColor = getVScodeColor('--vscode-list-hoverBackground')||'#2a2d2e';
return themes.DARK.extends({
defaultStyle: {
hover: {
cellBgColor: hoverBgColor
}
},
frameStyle: {
// borderColor: '#000000',
cornerRadius: 0,
borderLineWidth: 0,
shadowBlur: 0
},
bodyStyle: {
bgColor: getBackgroundColor,
hover: {
cellBgColor: hoverBgColor,
inlineRowBgColor: hoverBgColor,
inlineColumnBgColor: hoverBgColor
},
},
headerStyle: {
bgColor: getVScodeColor('--x-thead-background')||'#1e1e1e',
hover: {
cellBgColor: hoverBgColor,
inlineRowBgColor: hoverBgColor,
},
},
})
}
return themes.DEFAULT.extends({
frameStyle: {
cornerRadius: 0,
borderLineWidth: 0,
},
headerStyle: {
bgColor: getVScodeColor('--x-thead-background')||'#f3f3f3'
}
})
}
const LoadingComponent = () => (
<div style={{ height: '100%', width: '100%', top: 0, left: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
<Spin size='large'/>
</div>
);
const EmptyComponent = () => (
<div style={{ height: '100%', width: '100%', top: 0, left: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none' }}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
</div>
);
const QueryResult = forwardRef((props, ref) => {
const dispatch = useDispatch();
const iSqlResult = useSelector(sqlResult);
const iVsTheme = useSelector(vsTheme);
const tableRef = useRef();
const tableInstance = useRef();
const iExecuteSqlStatus = useSelector(executeSqlStatus);
const getColums = (data)=>{
if (!data || !data.length) return [];
return Object.keys(data[0]).map(key => ({
title: key,
field: key,
key: key,
width: 'auto'
}));
}
const getTableOptions = ()=>{
return {
theme: getThemes(iVsTheme),
hover:{
highlightMode: 'cross'
},
keyboardOptions: {
copySelected: true,
},
menu: {
contextMenuItems: ['Copy']
},
showFrozenIcon: true,
}
}
useEffect(()=>{
const tableOptions = getTableOptions();
tableInstance.current = new ListTable(tableRef.current, tableOptions);
let copyData;
tableInstance.current.on('dropdown_menu_click', args => {
// console.log('dropdown_menu_click', args);
if (args.menuKey === 'Copy') {
copyData = tableInstance.current.getCopyValue();
// console.log('copyData:', copyData);
dispatch(writeToClipboardAsync({ content: copyData }));
}
})
}, [])
useEffect(() => {
if (iSqlResult) {
const iColums = getColums(iSqlResult)
// const tbOptions = { ...getTableOptions(), allowFrozenColCount: iColums.length>10?3:0 };
// tableInstance.current.updateOption(tbOptions);
tableInstance.current.updateColumns(iColums);
tableInstance.current.setRecords(iSqlResult);
tableInstance.current.clearSelected();
}
}, [iSqlResult]);
useEffect(() => {
tableInstance.current.updateTheme(getThemes(iVsTheme));
}, [iVsTheme]);
const tableDisplay = useMemo(() => {
return iExecuteSqlStatus === 'loading'||(!iSqlResult||iSqlResult.length===0) ? 'none' : 'block';
}, [iExecuteSqlStatus, iSqlResult]);
const loadingDisplay = useMemo(() => {
return iExecuteSqlStatus === 'loading';
}, [iExecuteSqlStatus]);
const emptyDisplay = useMemo(() => {
return (iExecuteSqlStatus !== 'loading')&&(!iSqlResult||iSqlResult.length===0);
}, [iExecuteSqlStatus, iSqlResult]);
useImperativeHandle(ref, () => ({
table: tableInstance.current,
exportToExcel: async () => {
if(!iSqlResult||iSqlResult.length===0) {
return
}
if (tableInstance.current) {
const exceldata = await exportVTableToExcel(tableInstance.current, { exportAllData: true });
const fileStr = Buffer.from(exceldata).toString('base64');
dispatch(exportToSaveAsync({ fileStr: fileStr, fileType: 'excel' }));
}
},
exportToCsv: async () => {
console.log('iSqlResult:', iSqlResult)
if(!iSqlResult||iSqlResult.length===0) {
return
}
if (tableInstance.current) {
const csvdata = await exportVTableToCsv(tableInstance.current, { exportAllData: true });
dispatch(exportToSaveAsync({ fileStr: csvdata, fileType: 'csv' }));
}
}
}));
return (
<div style={{ height: '100%', width: '100%' }}>
{ loadingDisplay?<LoadingComponent /> : null }
{ emptyDisplay?<EmptyComponent /> : null }
<div style={{ width: '100%', height: '100%', display: tableDisplay }} ref={tableRef}>
</div>
</div>
);
});
export default QueryResult;
```
## /ui/src/features/sqlEditor/TableTree/index.jsx
```jsx path="/ui/src/features/sqlEditor/TableTree/index.jsx"
import { useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Card, Tree, Empty, Tag, Space } from 'antd';
import { KeyOutlined, TableOutlined } from '@ant-design/icons';
import { tableTree, tableTreeStatus, getAllTablesAsync, executeSqlAsync } from '../../../stores/sqlSlice';
const TableTree = () => {
const dispatch = useDispatch();
const iTableTree = useSelector(tableTree);
const iTableTreeStatus = useSelector(tableTreeStatus);
useEffect(() => {
dispatch(getAllTablesAsync());
}, [dispatch]);
const treeData = useMemo(() => {
return iTableTree.map((item) => ({
title: item.tableName,
key: item.tableName,
children: item.columns ? item.columns.map((child) => ({
title: (<Space>{child.name}<Tag>{child.type}</Tag></Space>),
key: `${item.tableName}-${child.cid}`,
icon: child.pk ? <KeyOutlined style={{ color: '#ffc53d' }} /> : null,
})) : [],
}));
}, [iTableTree]);
console.log('iTableTree-', iTableTree)
const handelSelectTable = (tableName) => {
const sql = `SELECT * FROM ${tableName};`;
dispatch(executeSqlAsync({ sql }));
};
const onDoubleClick = (event, node) => {
console.log('double click node:', node);
if (node && node.title && node.children && node.children.length > 0) {
// 双击表名,插入到编辑器
handelSelectTable(node.title);
}
};
return (
<Card
variant="borderless"
size="small"
style={{ height: '100%', boxShadow: 'none' }}
styles={{
body: { height: 'calc(100% - 38px)', overflow: 'auto' },
}}
loading={iTableTreeStatus === 'loading'}
title={<i style={{fontStyle:'normal'}}><TableOutlined/> Tables</i>}
>
{treeData.length > 0 ? (
<Tree
treeData={treeData}
autoExpandParent
blockNode
showLine
showIcon
selectable={false}
onDoubleClick={onDoubleClick}
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</Card>
);
};
export default TableTree;
```
## /ui/src/features/sqlEditor/index.jsx
```jsx path="/ui/src/features/sqlEditor/index.jsx"
import { useRef } from "react";
import { Splitter, Divider, Button, Dropdown } from 'antd';
import { ConsoleSqlOutlined, PlayCircleOutlined, StarOutlined, FileExcelOutlined, FileTextOutlined, DownOutlined, ExportOutlined } from '@ant-design/icons';
import Editor from "./Editor";
import QueryResult from './QueryResult';
import TableTree from "./TableTree";
import Favorite from "./Favorite";
const exportItems = [
{
label: 'CSV',
key: 'csv',
icon: <FileTextOutlined />,
},
{
label: 'Excel',
key: 'excel',
icon: <FileExcelOutlined />,
}
]
const SqlEditor = () => {
const editorRef = useRef(null);
const favoriteRef = useRef(null);
const queryResultRef = useRef(null);
const toolBar = [
<Button
size="small"
color="pink"
variant="outlined"
icon={<StarOutlined />}
onClick={() => favoriteRef.current.handleAddFavorite()}
>
Add
</Button>,
<Button
size="small"
icon={<ConsoleSqlOutlined />}
onClick={() => editorRef.current.format()}
>
Format
</Button>,
<Button
type="primary"
size="small"
icon={<PlayCircleOutlined />}
onClick={() => editorRef.current.executeSql()}
>
Run Query
</Button>,
<Divider type="vertical" key='splicv1' />,
<Dropdown
menu={{
items: exportItems,
onClick: ({ key }) => {
if (key === 'csv') {
queryResultRef.current.exportToCsv();
} else if (key === 'excel') {
queryResultRef.current.exportToExcel();
}
}
}}
key='export'
>
<Button size="small" icon={<ExportOutlined />}>
Export <DownOutlined />
</Button>
</Dropdown>
];
return (
<Splitter layout="vertical" style={{ height: '100%' }}>
<Splitter.Panel defaultSize={'45%'}>
<Splitter>
<Splitter.Panel>
<Editor toolBarExtra={toolBar} ref={editorRef} />
</Splitter.Panel>
<Splitter.Panel defaultSize={'20%'} collapsible={true}>
<Favorite ref={favoriteRef} editorRef={editorRef} />
</Splitter.Panel>
<Splitter.Panel defaultSize={'20%'}>
<TableTree />
</Splitter.Panel>
</Splitter>
</Splitter.Panel>
<Splitter.Panel>
<QueryResult ref={queryResultRef} />
</Splitter.Panel>
</Splitter>
);
};
export default SqlEditor;
```
## /ui/src/index.css
```css path="/ui/src/index.css"
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 0;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
```
## /ui/src/index.js
```js path="/ui/src/index.js"
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from "react-router-dom";
import { store } from './stores';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.css';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
reportWebVitals();
```
## /ui/src/logo.svg
```svg path="/ui/src/logo.svg"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
```
## /ui/src/reportWebVitals.js
```js path="/ui/src/reportWebVitals.js"
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
```
## /ui/src/setupTests.js
```js path="/ui/src/setupTests.js"
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
```
## /ui/src/stores/api.js
```js path="/ui/src/stores/api.js"
import { request } from './rpcRequest';
/**
* 获取表格树
* @returns
*/
export const getAllTables = () => {
return request({ method: 'GET_ALL_TABLES' });
}
/**
* 执行SQL
* @param {*} data
* @returns
*/
export const executeSql = (data) => {
return request({ method: 'EXECUTE_SQL', data });
}
/**
* 获取用户收藏的SQL语句列表
* @returns
*/
export const getUserFavorites = () => {
return request({ method: 'GET_USER_FAVORITES' });
}
/**
* 添加用户收藏的SQL语句
* @param {*} data { name, sql }
* @returns
*/
export const addUserFavorite = (data) => {
return request({ method: 'ADD_USER_FAVORITE', data });
}
/**
* 删除用户收藏的SQL语句
* @param {*} data { id }
* @returns
*/
export const removeUserFavorite = (data) => {
return request({ method: 'REMOVE_USER_FAVORITE', data });
}
/**
* 重命名用户收藏的SQL语句
* @param {*} data { id, name }
* @returns
*/
export const renameUserFavorite = (data) => {
return request({ method: 'RENAME_USER_FAVORITE', data });
}
/**
* 复制内容到剪贴板
* @param {*} data
* @returns
*/
export const writeToClipboard = (data) => {
return request({ method: 'COPY_TO_CLIPBOARD', data });
}
/**
* 查询结果导出为csv/excel
* @param {*} data
* @returns
*/
export const exportToSave = (data) => {
return request({ method: 'EXPORT_SQL_RESULT', data });
}
/**
* 排序用户收藏的SQL语句
* @param {*} data fidList
* @returns
*/
export const sortUserFavorites = (data) => {
return request({ method: 'SORT_USER_FAVORITES', data });
}
```
## /ui/src/stores/homeSlice.js
```js path="/ui/src/stores/homeSlice.js"
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { writeToClipboard } from './api';
const initialState = {
vsTheme: 'dark',
};
export const writeToClipboardAsync = createAsyncThunk(
"home/writeToClipboard",
async (data) => {
const response = await writeToClipboard(data);
return response.data;
}
);
export const slice = createSlice({
name: "home",
initialState,
reducers: {
setVsTheme: (state, action) => {
state.vsTheme = action.payload;
},
},
extraReducers: (builder) => {
}
});
export const vsTheme = (state) => state.home.vsTheme;
export const { setVsTheme } = slice.actions;
export default slice.reducer;
```
## /ui/src/stores/index.js
```js path="/ui/src/stores/index.js"
import { configureStore } from '@reduxjs/toolkit';
import homeSlice from './homeSlice';
import sqlSlice from './sqlSlice';
export const store = configureStore({
reducer: {
home: homeSlice,
sql: sqlSlice,
},
});
```
## /ui/src/stores/rpcRequest.js
```js path="/ui/src/stores/rpcRequest.js"
import { v1 } from 'uuid';
// import { testApi } from '../test/messageTest';
window.ddpMessage = {};
let vscode;
if (window.acquireVsCodeApi) {
vscode = window.acquireVsCodeApi();
} else {
// eslint-disable-next-line no-undef
// vscode = devMode? testApi: acquireVsCodeApi();
}
export const postMessage = (data) => {
if (vscode) {
vscode.postMessage(data);
return true;
}
// 非vscode环境
return false;
}
export const formatRequest = (method, data, id) => {
return {
id: id || v1(),
request: true,
method,
data,
hashkey: window['hashkey']
}
}
export const formatNotification = (method, data) => {
return {
notification: true,
method,
data,
hashkey: window['hashkey']
}
}
export const request = async ({ id, method, data }) => {
const requestData = formatRequest(method, data, id);
return new Promise((resolve, rejects) => {
if (postMessage(requestData)) {
console.log('Request', method, data);
window.ddpMessage[requestData.id] = resolve;
} else {
rejects({ code: 400, message: 'not in vscode' });
}
})
}
export const handelResponse = (data) => {
if (window.ddpMessage[data.id]) {
window.ddpMessage[data.id](data);
delete window.ddpMessage[data.id];
} else {
console.info('No handelResponse resolve:', data);
}
}
```
## /ui/src/stores/sqlSlice.js
```js path="/ui/src/stores/sqlSlice.js"
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { getAllTables, executeSql, getUserFavorites, addUserFavorite, renameUserFavorite, removeUserFavorite, exportToSave, sortUserFavorites } from './api';
const initialState = {
tableTree: [],
tableTreeStatus: 'idle',
executeSqlStatus: 'idle',
sqlResult: null,
favoriteList: [],
favoriteListStatus: 'idle',
};
export const getAllTablesAsync = createAsyncThunk(
"sql/getAllTables",
async () => {
const response = await getAllTables();
return response.data;
}
);
export const executeSqlAsync = createAsyncThunk(
"sql/executeSql",
async (data) => {
const response = await executeSql(data);
return response.data;
}
);
export const getUserFavoritesAsync = createAsyncThunk(
"sql/getUserFavorites",
async () => {
const response = await getUserFavorites();
return response.data;
}
);
export const addUserFavoriteAsync = createAsyncThunk(
"sql/addUserFavorite",
async (data) => {
const response = await addUserFavorite(data);
return response.data;
}
);
export const renameUserFavoriteAsync = createAsyncThunk(
"sql/renameUserFavorite",
async (data) => {
const response = await renameUserFavorite(data);
return response.data;
}
);
export const removeUserFavoriteAsync = createAsyncThunk(
"sql/removeUserFavorite",
async (data) => {
const response = await removeUserFavorite(data);
return response.data;
}
);
export const exportToSaveAsync = createAsyncThunk(
"sql/exportToSave",
async (data) => {
const response = await exportToSave(data);
return response.data;
}
);
export const sortUserFavoritesAsync = createAsyncThunk(
"sql/sortUserFavorites",
async (data) => {
const response = await sortUserFavorites(data);
return response.data;
}
);
export const slice = createSlice({
name: "sql",
initialState,
reducers: {
setFavoriteList: (state, action) => {
state.favoriteList = action.payload;
}
},
extraReducers: (builder) => {
builder
.addCase(getAllTablesAsync.pending, (state) => {
state.tableTreeStatus = 'loading';
})
.addCase(getAllTablesAsync.fulfilled, (state, action) => {
console.log('Fetched all tables:', action);
state.tableTreeStatus = 'idle';
state.tableTree = action.payload.data || [];
})
.addCase(executeSqlAsync.pending, (state) => {
state.executeSqlStatus = 'loading';
})
.addCase(executeSqlAsync.fulfilled, (state, action) => {
// console.log('Executed SQL:', action.payload);
state.executeSqlStatus = 'idle';
if (action.payload.errorCode === 200) {
state.sqlResult = action.payload.data || null;
}
})
.addCase(getUserFavoritesAsync.pending, (state) => {
state.favoriteListStatus = 'loading';
})
.addCase(getUserFavoritesAsync.fulfilled, (state, action) => {
console.log('Fetched user favorites:', action.payload);
state.favoriteListStatus = 'idle';
state.favoriteList = action.payload.data || [];
})
}
});
export const tableTree = (state) => state.sql.tableTree;
export const tableTreeStatus = (state) => state.sql.tableTreeStatus;
export const executeSqlStatus = (state) => state.sql.executeSqlStatus;
export const sqlResult = (state) => state.sql.sqlResult;
export const favoriteList = (state) => state.sql.favoriteList;
export const favoriteListStatus = (state) => state.sql.favoriteListStatus;
export const { setFavoriteList } = slice.actions;
export default slice.reducer;
```
## /ui/src/utils.js
```js path="/ui/src/utils.js"
/**
* 获取一个字符串()内的内容
* @param {string} str - 输入的字符串
* @returns
*/
const getBracketContent = (str) => {
const match = str.match(/\(([^)]+)\)/);
return match ? match[1] : '';
}
/**
* 字符串去除()以及括号内的内容
* @param {string} str - 输入的字符串
* @returns
*/
const removeBracketContent = (str) => {
return str.replace(/\s*\([^)]*\)/g, '').trim();
}
export function getTextWidthCanvas(text, font) {
let canvas = getTextWidthCanvas.canvas || (getTextWidthCanvas.canvas = document.createElement("canvas"));
let context = canvas.getContext("2d");
context.font = font;
let metrics = context.measureText(text);
// console.log('metrics---', metrics)
return metrics.width
}
/**
* 获取vscode主题色
* @param {*} propText
* @returns
*/
export function getVScodeColor(propText){
return getComputedStyle(document.documentElement).getPropertyValue(propText);
}
```
## /vsc-extension-quickstart.md
# 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.
* `src/extension.ts` - 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`.
## Setup
* install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint)
## 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 `src/extension.ts` 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 `src/extension.ts`.
* 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)
* Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered.
* 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 `src/test/extension.test.ts` or create new test files inside the `test` folder.
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
* You can create folders inside the `test` folder to structure your tests any way you want.
## Go further
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
* [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).
## /webpack.config.js
```js path="/webpack.config.js"
//@ts-check
'use strict';
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const os = require('os');
//@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'
}
]
},
{
exclude: /ui/
}
]
},
devtool: 'nosources-source-map',
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
plugins: [
new CopyPlugin({
patterns: [
{ from: path.resolve(__dirname, "node_modules/sqlite3/build/Release/node_sqlite3.node"), to: path.resolve(__dirname, `build/${os.type()}/${os.arch()}/node_sqlite3.node`) },
{ from: path.resolve(__dirname, "ui/build"), to: path.resolve(__dirname, 'ui_dist') }
]
})
],
};
module.exports = [ extensionConfig ];
```
The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.