sqlshell 0.1.6__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlshell might be problematic. Click here for more details.
- sqlshell-0.1.8/MANIFEST.in +10 -0
- sqlshell-0.1.8/PKG-INFO +120 -0
- sqlshell-0.1.8/README.md +91 -0
- sqlshell-0.1.8/pool.db +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/pyproject.toml +1 -1
- {sqlshell-0.1.6 → sqlshell-0.1.8}/setup.py +8 -1
- sqlshell-0.1.8/sqlshell/__init__.py +8 -0
- sqlshell-0.1.8/sqlshell/create_test_data.py +50 -0
- sqlshell-0.1.8/sqlshell/editor.py +355 -0
- sqlshell-0.1.8/sqlshell/main.py +1833 -0
- sqlshell-0.1.8/sqlshell/resources/__init__.py +1 -0
- sqlshell-0.1.8/sqlshell/resources/create_icon.py +53 -0
- sqlshell-0.1.8/sqlshell/resources/create_splash.py +66 -0
- sqlshell-0.1.8/sqlshell/resources/splash_screen.gif +0 -0
- sqlshell-0.1.8/sqlshell/splash_screen.py +177 -0
- sqlshell-0.1.8/sqlshell/sqlshell/create_test_data.py +118 -0
- sqlshell-0.1.8/sqlshell/sqlshell_demo.png +0 -0
- sqlshell-0.1.8/sqlshell/syntax_highlighter.py +123 -0
- sqlshell-0.1.8/sqlshell.egg-info/PKG-INFO +120 -0
- sqlshell-0.1.8/sqlshell.egg-info/SOURCES.txt +63 -0
- sqlshell-0.1.8/sqlshell_demo.png +0 -0
- sqlshell-0.1.8/sqlshell_logo.png +0 -0
- sqlshell-0.1.8/test_data/by_category.parquet +0 -0
- sqlshell-0.1.8/test_data/city_coordinates.csv +9 -0
- sqlshell-0.1.8/test_data/city_coordinates.parquet +0 -0
- sqlshell-0.1.8/test_data/city_coordinates.xlsx +0 -0
- sqlshell-0.1.8/test_data/customer_data.csv +101 -0
- sqlshell-0.1.8/test_data/customer_data.parquet +0 -0
- sqlshell-0.1.8/test_data/customer_data.xlsx +0 -0
- sqlshell-0.1.8/test_data/customers_by_country.parquet +0 -0
- sqlshell-0.1.8/test_data/delete_me.parquet +0 -0
- sqlshell-0.1.8/test_data/joined_test.parquet +0 -0
- sqlshell-0.1.8/test_data/price_by_category.parquet +0 -0
- sqlshell-0.1.8/test_data/product_catalog.csv +51 -0
- sqlshell-0.1.8/test_data/product_catalog.parquet +0 -0
- sqlshell-0.1.8/test_data/product_catalog.xlsx +0 -0
- sqlshell-0.1.8/test_data/product_categories.csv +81 -0
- sqlshell-0.1.8/test_data/product_categories.parquet +0 -0
- sqlshell-0.1.8/test_data/product_categories.xlsx +0 -0
- sqlshell-0.1.8/test_data/sample_sales_data.csv +1001 -0
- sqlshell-0.1.8/test_data/sample_sales_data.parquet +0 -0
- sqlshell-0.1.8/test_data/sample_sales_data.xlsx +0 -0
- sqlshell-0.1.8/test_data/stock_data.csv +101 -0
- sqlshell-0.1.8/test_data/stock_data.parquet +0 -0
- sqlshell-0.1.8/test_data/stock_data.xlsx +0 -0
- sqlshell-0.1.8/test_data/test.db +0 -0
- sqlshell-0.1.8/test_data/test.duckdb +0 -0
- sqlshell-0.1.8/test_data/test2.parquet +0 -0
- sqlshell-0.1.8/test_data/test3.parquet +0 -0
- sqlshell-0.1.8/test_data/test3.xlsx +0 -0
- sqlshell-0.1.8/test_data/test4.xlsx +0 -0
- sqlshell-0.1.8/test_data/test5.xlsx +0 -0
- sqlshell-0.1.8/test_data/testproject.sqls +23 -0
- sqlshell-0.1.8/test_data/weather_measurements.csv +366 -0
- sqlshell-0.1.8/test_data/weather_measurements.parquet +0 -0
- sqlshell-0.1.8/test_data/weather_measurements.xlsx +0 -0
- sqlshell-0.1.6/PKG-INFO +0 -92
- sqlshell-0.1.6/README.md +0 -63
- sqlshell-0.1.6/sqlshell/__init__.py +0 -6
- sqlshell-0.1.6/sqlshell/main.py +0 -1473
- sqlshell-0.1.6/sqlshell.egg-info/PKG-INFO +0 -92
- sqlshell-0.1.6/sqlshell.egg-info/SOURCES.txt +0 -15
- {sqlshell-0.1.6 → sqlshell-0.1.8}/setup.cfg +0 -0
- {sqlshell-0.1.6/sqlshell/sqlshell → sqlshell-0.1.8/sqlshell/data}/create_test_data.py +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell/setup.py +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell/sqlshell/__init__.py +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell/sqlshell/create_test_databases.py +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/dependency_links.txt +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/entry_points.txt +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/requires.txt +0 -0
- {sqlshell-0.1.6 → sqlshell-0.1.8}/sqlshell.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include sqlshell_logo.png
|
|
4
|
+
include sqlshell_demo.png
|
|
5
|
+
include pool.db
|
|
6
|
+
recursive-include sqlshell/resources *
|
|
7
|
+
recursive-include sqlshell/data *
|
|
8
|
+
recursive-include test_data *
|
|
9
|
+
global-exclude __pycache__
|
|
10
|
+
global-exclude *.py[cod]
|
sqlshell-0.1.8/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlshell
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: A powerful SQL shell with GUI interface for data analysis
|
|
5
|
+
Home-page: https://github.com/yourusername/sqlshell
|
|
6
|
+
Author: SQLShell Team
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/oyvinrog/SQLShell
|
|
9
|
+
Keywords: sql,data analysis,gui,duckdb
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pandas>=2.0.0
|
|
20
|
+
Requires-Dist: numpy>=1.24.0
|
|
21
|
+
Requires-Dist: PyQt6>=6.4.0
|
|
22
|
+
Requires-Dist: duckdb>=0.9.0
|
|
23
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
24
|
+
Requires-Dist: pyarrow>=14.0.1
|
|
25
|
+
Requires-Dist: fastparquet>=2023.10.1
|
|
26
|
+
Requires-Dist: xlrd>=2.0.1
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
|
|
30
|
+
# SQLShell
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
**A modern SQL REPL interface for seamless querying of Excel, Parquet, and SQLite databases**
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
## 🚀 Key Features
|
|
43
|
+
|
|
44
|
+
- **Interactive SQL Interface** - Rich syntax highlighting for enhanced query writing
|
|
45
|
+
- **DuckDB Integration** - Built-in support for local DuckDB database (pool.db)
|
|
46
|
+
- **Multi-Format Support** - Import and query Excel (.xlsx, .xls) and CSV files effortlessly
|
|
47
|
+
- **Modern UI** - Clean, tabular results display with intuitive controls
|
|
48
|
+
- **Productivity Tools** - Streamlined workflow with keyboard shortcuts (e.g., Ctrl+Enter for query execution)
|
|
49
|
+
|
|
50
|
+
## 📦 Installation
|
|
51
|
+
|
|
52
|
+
### Linux Setup with Virtual Environment
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Create and activate virtual environment
|
|
56
|
+
python3 -m venv ~/.venv/sqlshell
|
|
57
|
+
source ~/.venv/sqlshell/bin/activate
|
|
58
|
+
|
|
59
|
+
# Install SQLShell
|
|
60
|
+
pip install sqlshell
|
|
61
|
+
|
|
62
|
+
# Configure shell alias
|
|
63
|
+
echo 'alias sqls="~/.venv/sqlshell/bin/sqls"' >> ~/.bashrc # or ~/.zshrc for Zsh
|
|
64
|
+
source ~/.bashrc # or source ~/.zshrc
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Windows Quick Start
|
|
68
|
+
SQLShell is immediately available via the `sqls` command after installation:
|
|
69
|
+
```bash
|
|
70
|
+
pip install sqlshell
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 🎯 Getting Started
|
|
74
|
+
|
|
75
|
+
1. **Launch the Application**
|
|
76
|
+
```bash
|
|
77
|
+
sqls
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
2. **Database Connection**
|
|
81
|
+
- SQLShell automatically connects to a local DuckDB database named 'pool.db'
|
|
82
|
+
|
|
83
|
+
3. **Working with Excel Files**
|
|
84
|
+
- Click "Browse Excel" to select your file
|
|
85
|
+
- File contents are loaded as 'imported_data' table
|
|
86
|
+
- Query using standard SQL syntax
|
|
87
|
+
|
|
88
|
+
4. **Query Execution**
|
|
89
|
+
- Enter SQL in the editor
|
|
90
|
+
- Execute using Ctrl+Enter or the "Execute" button
|
|
91
|
+
- View results in the structured output panel
|
|
92
|
+
|
|
93
|
+
## 📝 Query Examples
|
|
94
|
+
|
|
95
|
+
### Basic Join Operation
|
|
96
|
+
```sql
|
|
97
|
+
SELECT *
|
|
98
|
+
FROM sample_sales_data cd
|
|
99
|
+
INNER JOIN product_catalog pc ON pc.productid = cd.productid
|
|
100
|
+
LIMIT 3;
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Multi-Statement Queries
|
|
104
|
+
```sql
|
|
105
|
+
-- Create a temporary view
|
|
106
|
+
CREATE OR REPLACE TEMPORARY VIEW test_v AS
|
|
107
|
+
SELECT *
|
|
108
|
+
FROM sample_sales_data cd
|
|
109
|
+
INNER JOIN product_catalog pc ON pc.productid = cd.productid;
|
|
110
|
+
|
|
111
|
+
-- Query the view
|
|
112
|
+
SELECT DISTINCT productid
|
|
113
|
+
FROM test_v;
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 💡 Pro Tips
|
|
117
|
+
|
|
118
|
+
- Use temporary views for complex query organization
|
|
119
|
+
- Leverage keyboard shortcuts for efficient workflow
|
|
120
|
+
- Explore the multi-format support for various data sources
|
sqlshell-0.1.8/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# SQLShell
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
**A modern SQL REPL interface for seamless querying of Excel, Parquet, and SQLite databases**
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
## 🚀 Key Features
|
|
14
|
+
|
|
15
|
+
- **Interactive SQL Interface** - Rich syntax highlighting for enhanced query writing
|
|
16
|
+
- **DuckDB Integration** - Built-in support for local DuckDB database (pool.db)
|
|
17
|
+
- **Multi-Format Support** - Import and query Excel (.xlsx, .xls) and CSV files effortlessly
|
|
18
|
+
- **Modern UI** - Clean, tabular results display with intuitive controls
|
|
19
|
+
- **Productivity Tools** - Streamlined workflow with keyboard shortcuts (e.g., Ctrl+Enter for query execution)
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
### Linux Setup with Virtual Environment
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Create and activate virtual environment
|
|
27
|
+
python3 -m venv ~/.venv/sqlshell
|
|
28
|
+
source ~/.venv/sqlshell/bin/activate
|
|
29
|
+
|
|
30
|
+
# Install SQLShell
|
|
31
|
+
pip install sqlshell
|
|
32
|
+
|
|
33
|
+
# Configure shell alias
|
|
34
|
+
echo 'alias sqls="~/.venv/sqlshell/bin/sqls"' >> ~/.bashrc # or ~/.zshrc for Zsh
|
|
35
|
+
source ~/.bashrc # or source ~/.zshrc
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Windows Quick Start
|
|
39
|
+
SQLShell is immediately available via the `sqls` command after installation:
|
|
40
|
+
```bash
|
|
41
|
+
pip install sqlshell
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 🎯 Getting Started
|
|
45
|
+
|
|
46
|
+
1. **Launch the Application**
|
|
47
|
+
```bash
|
|
48
|
+
sqls
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
2. **Database Connection**
|
|
52
|
+
- SQLShell automatically connects to a local DuckDB database named 'pool.db'
|
|
53
|
+
|
|
54
|
+
3. **Working with Excel Files**
|
|
55
|
+
- Click "Browse Excel" to select your file
|
|
56
|
+
- File contents are loaded as 'imported_data' table
|
|
57
|
+
- Query using standard SQL syntax
|
|
58
|
+
|
|
59
|
+
4. **Query Execution**
|
|
60
|
+
- Enter SQL in the editor
|
|
61
|
+
- Execute using Ctrl+Enter or the "Execute" button
|
|
62
|
+
- View results in the structured output panel
|
|
63
|
+
|
|
64
|
+
## 📝 Query Examples
|
|
65
|
+
|
|
66
|
+
### Basic Join Operation
|
|
67
|
+
```sql
|
|
68
|
+
SELECT *
|
|
69
|
+
FROM sample_sales_data cd
|
|
70
|
+
INNER JOIN product_catalog pc ON pc.productid = cd.productid
|
|
71
|
+
LIMIT 3;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Multi-Statement Queries
|
|
75
|
+
```sql
|
|
76
|
+
-- Create a temporary view
|
|
77
|
+
CREATE OR REPLACE TEMPORARY VIEW test_v AS
|
|
78
|
+
SELECT *
|
|
79
|
+
FROM sample_sales_data cd
|
|
80
|
+
INNER JOIN product_catalog pc ON pc.productid = cd.productid;
|
|
81
|
+
|
|
82
|
+
-- Query the view
|
|
83
|
+
SELECT DISTINCT productid
|
|
84
|
+
FROM test_v;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 💡 Pro Tips
|
|
88
|
+
|
|
89
|
+
- Use temporary views for complex query organization
|
|
90
|
+
- Leverage keyboard shortcuts for efficient workflow
|
|
91
|
+
- Explore the multi-format support for various data sources
|
sqlshell-0.1.8/pool.db
ADDED
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
def create_sales_data(num_records=1000):
|
|
6
|
+
"""Create sample sales data"""
|
|
7
|
+
# Generate random dates within the last year
|
|
8
|
+
end_date = datetime.now()
|
|
9
|
+
start_date = end_date - timedelta(days=365)
|
|
10
|
+
dates = pd.date_range(start=start_date, end=end_date, periods=num_records)
|
|
11
|
+
|
|
12
|
+
# Generate random data
|
|
13
|
+
data = {
|
|
14
|
+
'orderid': range(1, num_records + 1),
|
|
15
|
+
'orderdate': dates,
|
|
16
|
+
'customerid': np.random.randint(1, 101, num_records),
|
|
17
|
+
'productid': np.random.randint(1, 51, num_records),
|
|
18
|
+
'quantity': np.random.randint(1, 11, num_records),
|
|
19
|
+
'unitprice': np.random.uniform(10.0, 1000.0, num_records).round(2)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return pd.DataFrame(data)
|
|
23
|
+
|
|
24
|
+
def create_customer_data(num_customers=100):
|
|
25
|
+
"""Create sample customer data"""
|
|
26
|
+
# Generate random customer data
|
|
27
|
+
data = {
|
|
28
|
+
'customerid': range(1, num_customers + 1),
|
|
29
|
+
'customername': [f"Customer {i}" for i in range(1, num_customers + 1)],
|
|
30
|
+
'email': [f"customer{i}@example.com" for i in range(1, num_customers + 1)],
|
|
31
|
+
'country': np.random.choice(['USA', 'UK', 'Canada', 'Australia', 'Germany'], num_customers),
|
|
32
|
+
'joindate': pd.date_range(start='2020-01-01', periods=num_customers).tolist()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return pd.DataFrame(data)
|
|
36
|
+
|
|
37
|
+
def create_product_data(num_products=50):
|
|
38
|
+
"""Create sample product data"""
|
|
39
|
+
categories = ['Electronics', 'Books', 'Clothing', 'Home & Garden', 'Sports']
|
|
40
|
+
|
|
41
|
+
# Generate random product data
|
|
42
|
+
data = {
|
|
43
|
+
'productid': range(1, num_products + 1),
|
|
44
|
+
'productname': [f"Product {i}" for i in range(1, num_products + 1)],
|
|
45
|
+
'category': np.random.choice(categories, num_products),
|
|
46
|
+
'baseprice': np.random.uniform(5.0, 500.0, num_products).round(2),
|
|
47
|
+
'instock': np.random.choice([True, False], num_products, p=[0.8, 0.2])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return pd.DataFrame(data)
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import QPlainTextEdit, QWidget, QCompleter
|
|
2
|
+
from PyQt6.QtCore import Qt, QSize, QRect, QStringListModel
|
|
3
|
+
from PyQt6.QtGui import QFont, QColor, QTextCursor, QPainter, QBrush
|
|
4
|
+
|
|
5
|
+
class LineNumberArea(QWidget):
|
|
6
|
+
def __init__(self, editor):
|
|
7
|
+
super().__init__(editor)
|
|
8
|
+
self.editor = editor
|
|
9
|
+
|
|
10
|
+
def sizeHint(self):
|
|
11
|
+
return QSize(self.editor.line_number_area_width(), 0)
|
|
12
|
+
|
|
13
|
+
def paintEvent(self, event):
|
|
14
|
+
self.editor.line_number_area_paint_event(event)
|
|
15
|
+
|
|
16
|
+
class SQLEditor(QPlainTextEdit):
|
|
17
|
+
def __init__(self, parent=None):
|
|
18
|
+
super().__init__(parent)
|
|
19
|
+
self.line_number_area = LineNumberArea(self)
|
|
20
|
+
|
|
21
|
+
# Set monospaced font
|
|
22
|
+
font = QFont("Consolas", 12) # Increased font size for better readability
|
|
23
|
+
font.setFixedPitch(True)
|
|
24
|
+
self.setFont(font)
|
|
25
|
+
|
|
26
|
+
# Connect signals
|
|
27
|
+
self.blockCountChanged.connect(self.update_line_number_area_width)
|
|
28
|
+
self.updateRequest.connect(self.update_line_number_area)
|
|
29
|
+
|
|
30
|
+
# Initialize
|
|
31
|
+
self.update_line_number_area_width(0)
|
|
32
|
+
|
|
33
|
+
# Set tab width to 4 spaces
|
|
34
|
+
self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
|
|
35
|
+
|
|
36
|
+
# Set placeholder text
|
|
37
|
+
self.setPlaceholderText("Enter your SQL query here...")
|
|
38
|
+
|
|
39
|
+
# Initialize completer
|
|
40
|
+
self.completer = None
|
|
41
|
+
|
|
42
|
+
# SQL Keywords for autocomplete
|
|
43
|
+
self.sql_keywords = [
|
|
44
|
+
"SELECT", "FROM", "WHERE", "AND", "OR", "INNER", "OUTER", "LEFT", "RIGHT", "JOIN",
|
|
45
|
+
"ON", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "UNION", "EXCEPT", "INTERSECT",
|
|
46
|
+
"CREATE", "TABLE", "INDEX", "VIEW", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE",
|
|
47
|
+
"TRUNCATE", "ALTER", "ADD", "DROP", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
|
|
48
|
+
"UNIQUE", "NOT", "NULL", "IS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END",
|
|
49
|
+
"AS", "WITH", "BETWEEN", "LIKE", "IN", "EXISTS", "ALL", "ANY", "SOME", "DESC", "ASC",
|
|
50
|
+
"AVG", "COUNT", "SUM", "MAX", "MIN", "COALESCE", "CAST", "CONVERT"
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Initialize with SQL keywords
|
|
54
|
+
self.set_completer(QCompleter(self.sql_keywords))
|
|
55
|
+
|
|
56
|
+
# Set modern selection color
|
|
57
|
+
self.selection_color = QColor("#3498DB")
|
|
58
|
+
self.selection_color.setAlpha(50) # Make it semi-transparent
|
|
59
|
+
|
|
60
|
+
def set_completer(self, completer):
|
|
61
|
+
"""Set the completer for the editor"""
|
|
62
|
+
if self.completer:
|
|
63
|
+
self.completer.disconnect(self)
|
|
64
|
+
|
|
65
|
+
self.completer = completer
|
|
66
|
+
|
|
67
|
+
if not self.completer:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self.completer.setWidget(self)
|
|
71
|
+
self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
|
72
|
+
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
|
73
|
+
self.completer.activated.connect(self.insert_completion)
|
|
74
|
+
|
|
75
|
+
def update_completer_model(self, words):
|
|
76
|
+
"""Update the completer model with new words"""
|
|
77
|
+
if not self.completer:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Combine SQL keywords with table/column names
|
|
81
|
+
all_words = self.sql_keywords + words
|
|
82
|
+
|
|
83
|
+
# Create a model with all words
|
|
84
|
+
model = QStringListModel()
|
|
85
|
+
model.setStringList(all_words)
|
|
86
|
+
|
|
87
|
+
# Set the model to the completer
|
|
88
|
+
self.completer.setModel(model)
|
|
89
|
+
|
|
90
|
+
def text_under_cursor(self):
|
|
91
|
+
"""Get the text under the cursor for completion"""
|
|
92
|
+
tc = self.textCursor()
|
|
93
|
+
tc.select(QTextCursor.SelectionType.WordUnderCursor)
|
|
94
|
+
return tc.selectedText()
|
|
95
|
+
|
|
96
|
+
def insert_completion(self, completion):
|
|
97
|
+
"""Insert the completion text"""
|
|
98
|
+
if self.completer.widget() != self:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
tc = self.textCursor()
|
|
102
|
+
extra = len(completion) - len(self.completer.completionPrefix())
|
|
103
|
+
tc.movePosition(QTextCursor.MoveOperation.Left)
|
|
104
|
+
tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
|
|
105
|
+
tc.insertText(completion[-extra:] + " ")
|
|
106
|
+
self.setTextCursor(tc)
|
|
107
|
+
|
|
108
|
+
def complete(self):
|
|
109
|
+
"""Show completion popup"""
|
|
110
|
+
prefix = self.text_under_cursor()
|
|
111
|
+
|
|
112
|
+
if not prefix or len(prefix) < 2: # Only show completions for words with at least 2 characters
|
|
113
|
+
if self.completer.popup().isVisible():
|
|
114
|
+
self.completer.popup().hide()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
self.completer.setCompletionPrefix(prefix)
|
|
118
|
+
|
|
119
|
+
# If no completions, hide popup
|
|
120
|
+
if self.completer.completionCount() == 0:
|
|
121
|
+
self.completer.popup().hide()
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Get popup and position it under the current text
|
|
125
|
+
popup = self.completer.popup()
|
|
126
|
+
popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
|
|
127
|
+
|
|
128
|
+
# Calculate position for the popup
|
|
129
|
+
cr = self.cursorRect()
|
|
130
|
+
cr.setWidth(self.completer.popup().sizeHintForColumn(0) +
|
|
131
|
+
self.completer.popup().verticalScrollBar().sizeHint().width())
|
|
132
|
+
|
|
133
|
+
# Show the popup
|
|
134
|
+
self.completer.complete(cr)
|
|
135
|
+
|
|
136
|
+
def keyPressEvent(self, event):
|
|
137
|
+
# Handle completer popup navigation
|
|
138
|
+
if self.completer and self.completer.popup().isVisible():
|
|
139
|
+
# Handle navigation keys for the popup
|
|
140
|
+
if event.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Tab,
|
|
141
|
+
Qt.Key.Key_Escape, Qt.Key.Key_Up, Qt.Key.Key_Down]:
|
|
142
|
+
event.ignore()
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Handle special key combinations
|
|
146
|
+
if event.key() == Qt.Key.Key_Tab:
|
|
147
|
+
# Insert 4 spaces instead of a tab character
|
|
148
|
+
self.insertPlainText(" ")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
# Auto-indentation for new lines
|
|
152
|
+
if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
|
|
153
|
+
cursor = self.textCursor()
|
|
154
|
+
block = cursor.block()
|
|
155
|
+
text = block.text()
|
|
156
|
+
|
|
157
|
+
# Get the indentation of the current line
|
|
158
|
+
indentation = ""
|
|
159
|
+
for char in text:
|
|
160
|
+
if char.isspace():
|
|
161
|
+
indentation += char
|
|
162
|
+
else:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
# Check if line ends with an opening bracket or keywords that should increase indentation
|
|
166
|
+
increase_indent = ""
|
|
167
|
+
if text.strip().endswith("(") or any(text.strip().upper().endswith(keyword) for keyword in
|
|
168
|
+
["SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING"]):
|
|
169
|
+
increase_indent = " "
|
|
170
|
+
|
|
171
|
+
# Insert new line with proper indentation
|
|
172
|
+
super().keyPressEvent(event)
|
|
173
|
+
self.insertPlainText(indentation + increase_indent)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# Handle keyboard shortcuts
|
|
177
|
+
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
|
|
178
|
+
if event.key() == Qt.Key.Key_Space:
|
|
179
|
+
# Show completion popup
|
|
180
|
+
self.complete()
|
|
181
|
+
return
|
|
182
|
+
elif event.key() == Qt.Key.Key_K:
|
|
183
|
+
# Comment/uncomment the selected lines
|
|
184
|
+
self.toggle_comment()
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# For normal key presses
|
|
188
|
+
super().keyPressEvent(event)
|
|
189
|
+
|
|
190
|
+
# Check for autocomplete after typing
|
|
191
|
+
if event.text() and not event.text().isspace():
|
|
192
|
+
self.complete()
|
|
193
|
+
|
|
194
|
+
def paintEvent(self, event):
|
|
195
|
+
# Call the parent's paintEvent first
|
|
196
|
+
super().paintEvent(event)
|
|
197
|
+
|
|
198
|
+
# Get the current cursor
|
|
199
|
+
cursor = self.textCursor()
|
|
200
|
+
|
|
201
|
+
# If there's a selection, paint custom highlight
|
|
202
|
+
if cursor.hasSelection():
|
|
203
|
+
# Create a painter for this widget
|
|
204
|
+
painter = QPainter(self.viewport())
|
|
205
|
+
|
|
206
|
+
# Get the selection start and end positions
|
|
207
|
+
start = cursor.selectionStart()
|
|
208
|
+
end = cursor.selectionEnd()
|
|
209
|
+
|
|
210
|
+
# Create temporary cursor to get the rectangles
|
|
211
|
+
temp_cursor = QTextCursor(cursor)
|
|
212
|
+
|
|
213
|
+
# Move to start and get the starting position
|
|
214
|
+
temp_cursor.setPosition(start)
|
|
215
|
+
start_pos = self.cursorRect(temp_cursor)
|
|
216
|
+
|
|
217
|
+
# Move to end and get the ending position
|
|
218
|
+
temp_cursor.setPosition(end)
|
|
219
|
+
end_pos = self.cursorRect(temp_cursor)
|
|
220
|
+
|
|
221
|
+
# Set the highlight color with transparency
|
|
222
|
+
painter.setBrush(QBrush(self.selection_color))
|
|
223
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
224
|
+
|
|
225
|
+
# Draw the highlight rectangle
|
|
226
|
+
if start_pos.top() == end_pos.top():
|
|
227
|
+
# Single line selection
|
|
228
|
+
painter.drawRect(QRect(start_pos.left(), start_pos.top(),
|
|
229
|
+
end_pos.right() - start_pos.left(), start_pos.height()))
|
|
230
|
+
else:
|
|
231
|
+
# Multi-line selection
|
|
232
|
+
# First line
|
|
233
|
+
painter.drawRect(QRect(start_pos.left(), start_pos.top(),
|
|
234
|
+
self.viewport().width() - start_pos.left(), start_pos.height()))
|
|
235
|
+
|
|
236
|
+
# Middle lines (if any)
|
|
237
|
+
if end_pos.top() > start_pos.top() + start_pos.height():
|
|
238
|
+
painter.drawRect(QRect(0, start_pos.top() + start_pos.height(),
|
|
239
|
+
self.viewport().width(),
|
|
240
|
+
end_pos.top() - (start_pos.top() + start_pos.height())))
|
|
241
|
+
|
|
242
|
+
# Last line
|
|
243
|
+
painter.drawRect(QRect(0, end_pos.top(), end_pos.right(), end_pos.height()))
|
|
244
|
+
|
|
245
|
+
painter.end()
|
|
246
|
+
|
|
247
|
+
def focusInEvent(self, event):
|
|
248
|
+
super().focusInEvent(event)
|
|
249
|
+
# Show temporary hint in status bar when editor gets focus
|
|
250
|
+
if hasattr(self.parent(), 'statusBar'):
|
|
251
|
+
self.parent().parent().parent().statusBar().showMessage('Press Ctrl+Space for autocomplete', 2000)
|
|
252
|
+
|
|
253
|
+
def toggle_comment(self):
|
|
254
|
+
cursor = self.textCursor()
|
|
255
|
+
if cursor.hasSelection():
|
|
256
|
+
# Get the selected text
|
|
257
|
+
start = cursor.selectionStart()
|
|
258
|
+
end = cursor.selectionEnd()
|
|
259
|
+
|
|
260
|
+
# Remember the selection
|
|
261
|
+
cursor.setPosition(start)
|
|
262
|
+
start_block = cursor.blockNumber()
|
|
263
|
+
cursor.setPosition(end)
|
|
264
|
+
end_block = cursor.blockNumber()
|
|
265
|
+
|
|
266
|
+
# Process each line in the selection
|
|
267
|
+
cursor.setPosition(start)
|
|
268
|
+
cursor.beginEditBlock()
|
|
269
|
+
|
|
270
|
+
for _ in range(start_block, end_block + 1):
|
|
271
|
+
# Move to start of line
|
|
272
|
+
cursor.movePosition(cursor.MoveOperation.StartOfLine)
|
|
273
|
+
|
|
274
|
+
# Check if the line is already commented
|
|
275
|
+
line_text = cursor.block().text().lstrip()
|
|
276
|
+
if line_text.startswith('--'):
|
|
277
|
+
# Remove comment
|
|
278
|
+
pos = cursor.block().text().find('--')
|
|
279
|
+
cursor.setPosition(cursor.block().position() + pos)
|
|
280
|
+
cursor.deleteChar()
|
|
281
|
+
cursor.deleteChar()
|
|
282
|
+
else:
|
|
283
|
+
# Add comment
|
|
284
|
+
cursor.insertText('--')
|
|
285
|
+
|
|
286
|
+
# Move to next line if not at the end
|
|
287
|
+
if not cursor.atEnd():
|
|
288
|
+
cursor.movePosition(cursor.MoveOperation.NextBlock)
|
|
289
|
+
|
|
290
|
+
cursor.endEditBlock()
|
|
291
|
+
else:
|
|
292
|
+
# Comment/uncomment current line
|
|
293
|
+
cursor.movePosition(cursor.MoveOperation.StartOfLine)
|
|
294
|
+
cursor.movePosition(cursor.MoveOperation.EndOfLine, cursor.MoveMode.KeepAnchor)
|
|
295
|
+
line_text = cursor.selectedText().lstrip()
|
|
296
|
+
|
|
297
|
+
cursor.movePosition(cursor.MoveOperation.StartOfLine)
|
|
298
|
+
if line_text.startswith('--'):
|
|
299
|
+
# Remove comment
|
|
300
|
+
pos = cursor.block().text().find('--')
|
|
301
|
+
cursor.setPosition(cursor.block().position() + pos)
|
|
302
|
+
cursor.deleteChar()
|
|
303
|
+
cursor.deleteChar()
|
|
304
|
+
else:
|
|
305
|
+
# Add comment
|
|
306
|
+
cursor.insertText('--')
|
|
307
|
+
|
|
308
|
+
def line_number_area_width(self):
|
|
309
|
+
digits = 1
|
|
310
|
+
max_num = max(1, self.blockCount())
|
|
311
|
+
while max_num >= 10:
|
|
312
|
+
max_num //= 10
|
|
313
|
+
digits += 1
|
|
314
|
+
|
|
315
|
+
space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
|
|
316
|
+
return space
|
|
317
|
+
|
|
318
|
+
def update_line_number_area_width(self, _):
|
|
319
|
+
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
|
|
320
|
+
|
|
321
|
+
def update_line_number_area(self, rect, dy):
|
|
322
|
+
if dy:
|
|
323
|
+
self.line_number_area.scroll(0, dy)
|
|
324
|
+
else:
|
|
325
|
+
self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
|
|
326
|
+
|
|
327
|
+
if rect.contains(self.viewport().rect()):
|
|
328
|
+
self.update_line_number_area_width(0)
|
|
329
|
+
|
|
330
|
+
def resizeEvent(self, event):
|
|
331
|
+
super().resizeEvent(event)
|
|
332
|
+
cr = self.contentsRect()
|
|
333
|
+
self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
|
|
334
|
+
|
|
335
|
+
def line_number_area_paint_event(self, event):
|
|
336
|
+
painter = QPainter(self.line_number_area)
|
|
337
|
+
painter.fillRect(event.rect(), QColor("#f0f0f0")) # Light gray background
|
|
338
|
+
|
|
339
|
+
block = self.firstVisibleBlock()
|
|
340
|
+
block_number = block.blockNumber()
|
|
341
|
+
top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
|
|
342
|
+
bottom = top + round(self.blockBoundingRect(block).height())
|
|
343
|
+
|
|
344
|
+
while block.isValid() and top <= event.rect().bottom():
|
|
345
|
+
if block.isVisible() and bottom >= event.rect().top():
|
|
346
|
+
number = str(block_number + 1)
|
|
347
|
+
painter.setPen(QColor("#808080")) # Gray text
|
|
348
|
+
painter.drawText(0, top, self.line_number_area.width() - 5,
|
|
349
|
+
self.fontMetrics().height(),
|
|
350
|
+
Qt.AlignmentFlag.AlignRight, number)
|
|
351
|
+
|
|
352
|
+
block = block.next()
|
|
353
|
+
top = bottom
|
|
354
|
+
bottom = top + round(self.blockBoundingRect(block).height())
|
|
355
|
+
block_number += 1
|