sqlshell 0.1.5__py3-none-any.whl → 0.1.8__py3-none-any.whl
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/__init__.py +4 -2
- sqlshell/create_test_data.py +50 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/editor.py +355 -0
- sqlshell/main.py +1510 -180
- sqlshell/resources/__init__.py +1 -0
- sqlshell/resources/create_icon.py +53 -0
- sqlshell/resources/create_splash.py +66 -0
- sqlshell/resources/splash_screen.gif +0 -0
- sqlshell/splash_screen.py +177 -0
- sqlshell/sqlshell/create_test_data.py +4 -23
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/syntax_highlighter.py +123 -0
- sqlshell-0.1.8.dist-info/METADATA +120 -0
- sqlshell-0.1.8.dist-info/RECORD +21 -0
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.8.dist-info}/WHEEL +1 -1
- sqlshell-0.1.5.dist-info/METADATA +0 -92
- sqlshell-0.1.5.dist-info/RECORD +0 -11
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.8.dist-info}/entry_points.txt +0 -0
- {sqlshell-0.1.5.dist-info → sqlshell-0.1.8.dist-info}/top_level.txt +0 -0
sqlshell/__init__.py
CHANGED
|
@@ -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,137 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
# Set random seed for reproducibility
|
|
7
|
+
np.random.seed(42)
|
|
8
|
+
|
|
9
|
+
# Define output directory
|
|
10
|
+
OUTPUT_DIR = 'test_data'
|
|
11
|
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
12
|
+
|
|
13
|
+
def create_sales_data(num_records=1000):
|
|
14
|
+
# Generate dates for the last 365 days
|
|
15
|
+
end_date = datetime.now()
|
|
16
|
+
start_date = end_date - timedelta(days=365)
|
|
17
|
+
dates = [start_date + timedelta(days=x) for x in range(366)]
|
|
18
|
+
random_dates = np.random.choice(dates, num_records)
|
|
19
|
+
|
|
20
|
+
# Create product data
|
|
21
|
+
products = ['Laptop', 'Smartphone', 'Tablet', 'Monitor', 'Keyboard', 'Mouse', 'Headphones', 'Printer']
|
|
22
|
+
product_prices = {
|
|
23
|
+
'Laptop': (800, 2000),
|
|
24
|
+
'Smartphone': (400, 1200),
|
|
25
|
+
'Tablet': (200, 800),
|
|
26
|
+
'Monitor': (150, 500),
|
|
27
|
+
'Keyboard': (20, 150),
|
|
28
|
+
'Mouse': (10, 80),
|
|
29
|
+
'Headphones': (30, 300),
|
|
30
|
+
'Printer': (100, 400)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Generate random data
|
|
34
|
+
data = {
|
|
35
|
+
'OrderID': range(1, num_records + 1),
|
|
36
|
+
'Date': random_dates,
|
|
37
|
+
'ProductID': np.random.randint(1, len(products) + 1, num_records), # Changed to ProductID for joining
|
|
38
|
+
'Quantity': np.random.randint(1, 11, num_records),
|
|
39
|
+
'CustomerID': np.random.randint(1, 201, num_records),
|
|
40
|
+
'Region': np.random.choice(['North', 'South', 'East', 'West'], num_records)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Calculate prices based on product
|
|
44
|
+
product_list = [products[pid-1] for pid in data['ProductID']]
|
|
45
|
+
data['Price'] = [np.random.uniform(product_prices[p][0], product_prices[p][1])
|
|
46
|
+
for p in product_list]
|
|
47
|
+
data['TotalAmount'] = [price * qty for price, qty in zip(data['Price'], data['Quantity'])]
|
|
48
|
+
|
|
49
|
+
# Create DataFrame
|
|
50
|
+
df = pd.DataFrame(data)
|
|
51
|
+
|
|
52
|
+
# Round numerical columns
|
|
53
|
+
df['Price'] = df['Price'].round(2)
|
|
54
|
+
df['TotalAmount'] = df['TotalAmount'].round(2)
|
|
55
|
+
|
|
56
|
+
# Sort by Date
|
|
57
|
+
return df.sort_values('Date')
|
|
58
|
+
|
|
59
|
+
def create_customer_data(num_customers=200):
|
|
60
|
+
# Generate customer data
|
|
61
|
+
data = {
|
|
62
|
+
'CustomerID': range(1, num_customers + 1),
|
|
63
|
+
'FirstName': [f'Customer{i}' for i in range(1, num_customers + 1)],
|
|
64
|
+
'LastName': [f'Lastname{i}' for i in range(1, num_customers + 1)],
|
|
65
|
+
'Email': [f'customer{i}@example.com' for i in range(1, num_customers + 1)],
|
|
66
|
+
'JoinDate': [datetime.now() - timedelta(days=np.random.randint(1, 1000))
|
|
67
|
+
for _ in range(num_customers)],
|
|
68
|
+
'CustomerType': np.random.choice(['Regular', 'Premium', 'VIP'], num_customers),
|
|
69
|
+
'CreditScore': np.random.randint(300, 851, num_customers)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return pd.DataFrame(data)
|
|
73
|
+
|
|
74
|
+
def create_product_data():
|
|
75
|
+
# Create detailed product information
|
|
76
|
+
products = {
|
|
77
|
+
'ProductID': range(1, 9),
|
|
78
|
+
'ProductName': ['Laptop', 'Smartphone', 'Tablet', 'Monitor', 'Keyboard', 'Mouse', 'Headphones', 'Printer'],
|
|
79
|
+
'Category': ['Computers', 'Mobile', 'Mobile', 'Accessories', 'Accessories', 'Accessories', 'Audio', 'Peripherals'],
|
|
80
|
+
'Brand': ['TechPro', 'MobileX', 'TabletCo', 'ViewMax', 'TypeMaster', 'ClickPro', 'SoundMax', 'PrintPro'],
|
|
81
|
+
'StockQuantity': np.random.randint(50, 500, 8),
|
|
82
|
+
'MinPrice': [800, 400, 200, 150, 20, 10, 30, 100],
|
|
83
|
+
'MaxPrice': [2000, 1200, 800, 500, 150, 80, 300, 400],
|
|
84
|
+
'Weight_kg': [2.5, 0.2, 0.5, 3.0, 0.8, 0.1, 0.3, 5.0],
|
|
85
|
+
'WarrantyMonths': [24, 12, 12, 36, 12, 12, 24, 12]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return pd.DataFrame(products)
|
|
89
|
+
|
|
90
|
+
if __name__ == '__main__':
|
|
91
|
+
# Create and save sales data
|
|
92
|
+
sales_df = create_sales_data()
|
|
93
|
+
sales_output = os.path.join(OUTPUT_DIR, 'sample_sales_data.xlsx')
|
|
94
|
+
sales_df.to_excel(sales_output, index=False)
|
|
95
|
+
print(f"Created sales data in '{sales_output}'")
|
|
96
|
+
print(f"Number of sales records: {len(sales_df)}")
|
|
97
|
+
|
|
98
|
+
# Create and save customer data as parquet
|
|
99
|
+
customer_df = create_customer_data()
|
|
100
|
+
customer_output = os.path.join(OUTPUT_DIR, 'customer_data.parquet')
|
|
101
|
+
customer_df.to_parquet(customer_output, index=False)
|
|
102
|
+
print(f"\nCreated customer data in '{customer_output}'")
|
|
103
|
+
print(f"Number of customers: {len(customer_df)}")
|
|
104
|
+
|
|
105
|
+
# Create and save product data
|
|
106
|
+
product_df = create_product_data()
|
|
107
|
+
product_output = os.path.join(OUTPUT_DIR, 'product_catalog.xlsx')
|
|
108
|
+
product_df.to_excel(product_output, index=False)
|
|
109
|
+
print(f"\nCreated product catalog in '{product_output}'")
|
|
110
|
+
print(f"Number of products: {len(product_df)}")
|
|
111
|
+
|
|
112
|
+
# Print sample queries
|
|
113
|
+
print("\nSample SQL queries for joining the data:")
|
|
114
|
+
print("""
|
|
115
|
+
-- Join sales with customer data
|
|
116
|
+
SELECT s.*, c.FirstName, c.LastName, c.CustomerType
|
|
117
|
+
FROM test_data.sample_sales_data s
|
|
118
|
+
JOIN test_data.customer_data c ON s.CustomerID = c.CustomerID;
|
|
119
|
+
|
|
120
|
+
-- Join sales with product data
|
|
121
|
+
SELECT s.*, p.ProductName, p.Category, p.Brand
|
|
122
|
+
FROM test_data.sample_sales_data s
|
|
123
|
+
JOIN test_data.product_catalog p ON s.ProductID = p.ProductID;
|
|
124
|
+
|
|
125
|
+
-- Three-way join with aggregation
|
|
126
|
+
SELECT
|
|
127
|
+
p.Category,
|
|
128
|
+
c.CustomerType,
|
|
129
|
+
COUNT(*) as NumOrders,
|
|
130
|
+
SUM(s.TotalAmount) as TotalRevenue,
|
|
131
|
+
AVG(s.Quantity) as AvgQuantity
|
|
132
|
+
FROM test_data.sample_sales_data s
|
|
133
|
+
JOIN test_data.customer_data c ON s.CustomerID = c.CustomerID
|
|
134
|
+
JOIN test_data.product_catalog p ON s.ProductID = p.ProductID
|
|
135
|
+
GROUP BY p.Category, c.CustomerType
|
|
136
|
+
ORDER BY p.Category, c.CustomerType;
|
|
137
|
+
""")
|
sqlshell/editor.py
ADDED
|
@@ -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
|