-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathstructure-extension.ts
More file actions
242 lines (212 loc) · 7.01 KB
/
Copy pathstructure-extension.ts
File metadata and controls
242 lines (212 loc) · 7.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { type Extension, RangeSet, StateEffect, StateField } from "@codemirror/state";
import { EditorView, GutterMarker, gutter, type ViewUpdate } from "@codemirror/view";
import type { SqlParser } from "./parser.js";
import { type SqlStatement, SqlStructureAnalyzer } from "./structure-analyzer.js";
export interface SqlGutterConfig {
/** Background color for the current statement indicator */
backgroundColor?: string;
/** Background color for invalid statements */
errorBackgroundColor?: string;
/** Width of the gutter marker in pixels */
width?: number;
/** Additional CSS class for the gutter */
className?: string;
/** Whether to show markers for invalid statements */
showInvalid?: boolean;
/** Function to determine when to hide the gutter */
whenHide?: (view: EditorView) => boolean;
/** Opacity for non-current statements */
inactiveOpacity?: number;
/** Hide gutter when editor is not focused */
hideWhenNotFocused?: boolean;
/** Opacity when editor is not focused (overrides hideWhenNotFocused if set) */
unfocusedOpacity?: number;
}
interface SqlGutterState {
currentStatement: SqlStatement | null;
allStatements: SqlStatement[];
cursorPosition: number;
isFocused: boolean;
}
// State effect for updating SQL statements
const updateSqlStatementsEffect = StateEffect.define<SqlGutterState>();
// State field to track current SQL statements
const sqlGutterStateField = StateField.define<SqlGutterState>({
create(): SqlGutterState {
return {
currentStatement: null,
allStatements: [],
cursorPosition: 0,
isFocused: true,
};
},
update(value, tr) {
for (const effect of tr.effects) {
if (effect.is(updateSqlStatementsEffect)) {
return effect.value;
}
}
return value;
},
});
class SqlGutterMarker extends GutterMarker {
constructor(
private config: SqlGutterConfig,
private isCurrent: boolean,
private isValid: boolean = true,
private isFocused: boolean = true,
) {
super();
}
toDOM(): HTMLElement {
const el = document.createElement("div");
el.className = "cm-sql-gutter-marker";
// Set background color based on state
let backgroundColor = this.config.backgroundColor || "#3b82f6";
if (!this.isValid && this.config.showInvalid !== false) {
backgroundColor = this.config.errorBackgroundColor || "#ef4444";
}
// Calculate opacity based on focus state
let opacity: string;
if (!this.isFocused) {
if (this.config.unfocusedOpacity !== undefined) {
opacity = this.config.unfocusedOpacity.toString();
} else if (this.config.hideWhenNotFocused) {
opacity = "0";
} else {
// Default behavior when not focused - use normal opacity
opacity = this.isCurrent ? "1" : (this.config.inactiveOpacity || "0.3").toString();
}
} else {
// Normal focused behavior
opacity = this.isCurrent ? "1" : (this.config.inactiveOpacity || "0.3").toString();
}
el.style.cssText = `
background: ${backgroundColor};
height: 100%;
width: 100%;
opacity: ${opacity};
transition: opacity 150ms ease-in-out;
border-radius: 1px;
`;
return el;
}
eq(other: SqlGutterMarker): boolean {
return (
this.isCurrent === other.isCurrent &&
this.isValid === other.isValid &&
this.isFocused === other.isFocused &&
this.config === other.config
);
}
}
function createSqlGutterMarkers(
view: EditorView,
config: SqlGutterConfig,
): RangeSet<SqlGutterMarker> {
let markers = RangeSet.empty;
// Check if gutter should be hidden
if (config.whenHide?.(view)) {
return markers;
}
const state = view.state.field(sqlGutterStateField, false);
if (!state) {
return markers;
}
const { currentStatement, allStatements, isFocused } = state;
try {
// Create markers for all statements
for (const statement of allStatements) {
const isCurrent =
currentStatement?.from === statement.from && currentStatement?.to === statement.to;
// Skip invalid statements if configured to hide them
if (!statement.isValid && config.showInvalid === false) {
continue;
}
const marker = new SqlGutterMarker(config, isCurrent, statement.isValid, isFocused);
// Add marker to each line of the statement
for (let lineNum = statement.lineFrom; lineNum <= statement.lineTo; lineNum++) {
try {
const line = view.state.doc.line(lineNum);
markers = markers.update({
add: [marker.range(line.from)],
});
} catch (e) {
// Handle edge cases where line numbers might be invalid
console.warn("SqlGutter: Invalid line number", lineNum, e);
}
}
}
} catch (error) {
console.warn("SqlGutter: Error creating markers", error);
}
return markers;
}
function createUpdateListener(analyzer: SqlStructureAnalyzer): Extension {
return EditorView.updateListener.of((update: ViewUpdate) => {
// Update on document changes, selection changes, or focus changes
if (!update.docChanged && !update.selectionSet && !update.focusChanged) {
return;
}
const { state } = update.view;
const { main } = state.selection;
const cursorPosition = main.head;
// Analyze the document for SQL statements
const allStatements = analyzer.analyzeDocument(state);
const currentStatement = analyzer.getStatementAtPosition(state, cursorPosition);
const newState: SqlGutterState = {
currentStatement,
allStatements,
cursorPosition,
isFocused: update.view.hasFocus,
};
// Dispatch the update
update.view.dispatch({
effects: updateSqlStatementsEffect.of(newState),
});
});
}
function createGutterTheme(config: SqlGutterConfig): Extension {
const gutterWidth = config.width || 3;
return EditorView.baseTheme({
".cm-sql-gutter": {
width: `${gutterWidth}px`,
minWidth: `${gutterWidth}px`,
},
".cm-sql-gutter .cm-gutterElement": {
width: `${gutterWidth}px`,
padding: "0",
margin: "0",
},
".cm-sql-gutter-marker": {
width: "100%",
height: "100%",
display: "block",
},
// Ensure line numbers have proper spacing when SQL gutter is present
".cm-lineNumbers .cm-gutterElement": {
paddingLeft: "8px",
paddingRight: "8px",
},
});
}
function createSqlGutter(config: SqlGutterConfig): Extension {
return gutter({
class: `cm-sql-gutter ${config.className || ""}`,
markers: (view: EditorView) => createSqlGutterMarkers(view, config),
});
}
/**
* Creates a SQL gutter extension that shows visual indicators for SQL statements
* based on cursor position. Highlights the current statement and shows dimmed
* indicators for other statements.
*/
export function sqlStructureGutter(parser: SqlParser, config: SqlGutterConfig = {}): Extension[] {
const analyzer = new SqlStructureAnalyzer(parser);
return [
sqlGutterStateField,
createUpdateListener(analyzer),
createGutterTheme(config),
createSqlGutter(config),
];
}