Skip to content

Commit 4ddae5b

Browse files
committed
Merge branch 'main' into sign-win
2 parents 88aa634 + d703157 commit 4ddae5b

3 files changed

Lines changed: 278 additions & 4 deletions

File tree

ggsql-wasm/demo/src/quarto/editor.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "vscode-oniguruma";
77
import { Registry, parseRawGrammar, type IGrammar } from "vscode-textmate";
88
import { WASM_BASE } from "../wasmBase";
9+
import { registerGgsqlLinks } from "./links";
910

1011
// Must be set before any Monaco editor is created
1112
(self as any).MonacoEnvironment = {
@@ -97,7 +98,7 @@ monaco.editor.defineTheme("ggsql-pygments", {
9798

9899
let languageRegistered = false;
99100

100-
async function ensureLanguageRegistered(): Promise<void> {
101+
async function ensureLanguageRegistered(siteRoot: string): Promise<void> {
101102
if (languageRegistered) return;
102103
languageRegistered = true;
103104

@@ -149,6 +150,8 @@ async function ensureLanguageRegistered(): Promise<void> {
149150
},
150151
});
151152
}
153+
154+
registerGgsqlLinks(siteRoot);
152155
}
153156

154157
// TextMate state wrapper for Monaco
@@ -185,9 +188,10 @@ function editorHeight(lineCount: number): number {
185188

186189
export async function createEditor(
187190
container: HTMLElement,
188-
initialValue: string
191+
initialValue: string,
192+
siteRoot: string = "./"
189193
): Promise<EditorInstance> {
190-
await ensureLanguageRegistered();
194+
await ensureLanguageRegistered(siteRoot);
191195

192196
const lineCount = initialValue.split("\n").length;
193197
container.style.height = editorHeight(lineCount) + "px";
@@ -198,6 +202,7 @@ export async function createEditor(
198202
theme: "ggsql-pygments",
199203
automaticLayout: true,
200204
minimap: { enabled: false },
205+
hover: { delay: 500 },
201206
fontSize: 13,
202207
lineNumbers: "on",
203208
glyphMargin: false,
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import * as monaco from "monaco-editor";
2+
3+
// ---------------------------------------------------------------------------
4+
// Types
5+
// ---------------------------------------------------------------------------
6+
7+
interface DocLink {
8+
url: string;
9+
label: string;
10+
}
11+
12+
interface LinkMatch {
13+
startCol: number; // 1-based column
14+
endCol: number; // 1-based column (exclusive)
15+
link: DocLink;
16+
}
17+
18+
// ---------------------------------------------------------------------------
19+
// Keyword → doc URL mappings
20+
// ---------------------------------------------------------------------------
21+
22+
const CLAUSE_LINKS: Record<string, DocLink> = {
23+
visualise: { url: "syntax/clause/visualise", label: "VISUALISE clause" },
24+
visualize: { url: "syntax/clause/visualise", label: "VISUALISE clause" },
25+
draw: { url: "syntax/clause/draw", label: "DRAW clause" },
26+
place: { url: "syntax/clause/place", label: "PLACE clause" },
27+
scale: { url: "syntax/clause/scale", label: "SCALE clause" },
28+
facet: { url: "syntax/clause/facet", label: "FACET clause" },
29+
project: { url: "syntax/clause/project", label: "PROJECT clause" },
30+
label: { url: "syntax/clause/label", label: "LABEL clause" },
31+
};
32+
33+
const GEOM_LINKS: Record<string, DocLink> = {
34+
point: { url: "syntax/layer/type/point", label: "point layer" },
35+
line: { url: "syntax/layer/type/line", label: "line layer" },
36+
path: { url: "syntax/layer/type/path", label: "path layer" },
37+
bar: { url: "syntax/layer/type/bar", label: "bar layer" },
38+
area: { url: "syntax/layer/type/area", label: "area layer" },
39+
rect: { url: "syntax/layer/type/rect", label: "rect layer" },
40+
polygon: { url: "syntax/layer/type/polygon", label: "polygon layer" },
41+
ribbon: { url: "syntax/layer/type/ribbon", label: "ribbon layer" },
42+
histogram: { url: "syntax/layer/type/histogram", label: "histogram layer" },
43+
density: { url: "syntax/layer/type/density", label: "density layer" },
44+
smooth: { url: "syntax/layer/type/smooth", label: "smooth layer" },
45+
boxplot: { url: "syntax/layer/type/boxplot", label: "boxplot layer" },
46+
violin: { url: "syntax/layer/type/violin", label: "violin layer" },
47+
text: { url: "syntax/layer/type/text", label: "text layer" },
48+
segment: { url: "syntax/layer/type/segment", label: "segment layer" },
49+
rule: { url: "syntax/layer/type/rule", label: "rule layer" },
50+
linear: { url: "syntax/layer/type/linear", label: "linear layer" },
51+
errorbar: { url: "syntax/layer/type/errorbar", label: "errorbar layer" },
52+
};
53+
54+
const COORD_LINKS: Record<string, DocLink> = {
55+
cartesian: { url: "syntax/coord/cartesian", label: "cartesian coordinates" },
56+
polar: { url: "syntax/coord/polar", label: "polar coordinates" },
57+
};
58+
59+
const SCALE_TYPE_LINKS: Record<string, DocLink> = {
60+
continuous: {
61+
url: "syntax/scale/type/continuous",
62+
label: "continuous scale",
63+
},
64+
discrete: { url: "syntax/scale/type/discrete", label: "discrete scale" },
65+
binned: { url: "syntax/scale/type/binned", label: "binned scale" },
66+
ordinal: { url: "syntax/scale/type/ordinal", label: "ordinal scale" },
67+
identity: { url: "syntax/scale/type/identity", label: "identity scale" },
68+
};
69+
70+
const AESTHETIC_LINKS: Record<string, DocLink> = {
71+
x: { url: "syntax/scale/aesthetic/0_position", label: "position aesthetic" },
72+
y: { url: "syntax/scale/aesthetic/0_position", label: "position aesthetic" },
73+
xmin: {
74+
url: "syntax/scale/aesthetic/0_position",
75+
label: "position aesthetic",
76+
},
77+
xmax: {
78+
url: "syntax/scale/aesthetic/0_position",
79+
label: "position aesthetic",
80+
},
81+
ymin: {
82+
url: "syntax/scale/aesthetic/0_position",
83+
label: "position aesthetic",
84+
},
85+
ymax: {
86+
url: "syntax/scale/aesthetic/0_position",
87+
label: "position aesthetic",
88+
},
89+
xend: {
90+
url: "syntax/scale/aesthetic/0_position",
91+
label: "position aesthetic",
92+
},
93+
yend: {
94+
url: "syntax/scale/aesthetic/0_position",
95+
label: "position aesthetic",
96+
},
97+
color: { url: "syntax/scale/aesthetic/1_color", label: "color aesthetic" },
98+
colour: { url: "syntax/scale/aesthetic/1_color", label: "color aesthetic" },
99+
fill: { url: "syntax/scale/aesthetic/1_color", label: "color aesthetic" },
100+
stroke: { url: "syntax/scale/aesthetic/1_color", label: "color aesthetic" },
101+
opacity: {
102+
url: "syntax/scale/aesthetic/2_opacity",
103+
label: "opacity aesthetic",
104+
},
105+
linetype: {
106+
url: "syntax/scale/aesthetic/linetype",
107+
label: "linetype aesthetic",
108+
},
109+
linewidth: {
110+
url: "syntax/scale/aesthetic/linewidth",
111+
label: "linewidth aesthetic",
112+
},
113+
shape: { url: "syntax/scale/aesthetic/shape", label: "shape aesthetic" },
114+
size: { url: "syntax/scale/aesthetic/size", label: "size aesthetic" },
115+
panel: {
116+
url: "syntax/scale/aesthetic/Z_faceting",
117+
label: "faceting aesthetic",
118+
},
119+
row: {
120+
url: "syntax/scale/aesthetic/Z_faceting",
121+
label: "faceting aesthetic",
122+
},
123+
column: {
124+
url: "syntax/scale/aesthetic/Z_faceting",
125+
label: "faceting aesthetic",
126+
},
127+
};
128+
129+
const POSITION_LINKS: Record<string, DocLink> = {
130+
stack: { url: "syntax/layer/position/stack", label: "stack position" },
131+
dodge: { url: "syntax/layer/position/dodge", label: "dodge position" },
132+
jitter: { url: "syntax/layer/position/jitter", label: "jitter position" },
133+
identity: {
134+
url: "syntax/layer/position/identity",
135+
label: "identity position",
136+
},
137+
};
138+
139+
// ---------------------------------------------------------------------------
140+
// Regex patterns
141+
// ---------------------------------------------------------------------------
142+
143+
const CLAUSE_RE =
144+
/\b(VISUALISE|VISUALIZE|DRAW|PLACE|SCALE|FACET|PROJECT|LABEL)\b/gi;
145+
146+
const GEOM_RE =
147+
/\b(?:DRAW|PLACE)\s+(point|line|path|bar|area|rect|polygon|ribbon|histogram|density|smooth|boxplot|violin|text|segment|rule|linear|errorbar)\b/gi;
148+
149+
const COORD_RE = /\bTO\s+(cartesian|polar)\b/gi;
150+
151+
const SCALE_TYPE_RE =
152+
/\bSCALE\s+(CONTINUOUS|DISCRETE|BINNED|ORDINAL|IDENTITY)\b/gi;
153+
154+
const AESTHETIC_AFTER_AS_RE =
155+
/\bAS\s+(x|y|xmin|xmax|ymin|ymax|xend|yend|color|colour|fill|stroke|opacity|size|shape|linetype|linewidth|panel|row|column)\b/gi;
156+
157+
const AESTHETIC_AFTER_SCALE_RE =
158+
/\bSCALE\s+(?:(?:CONTINUOUS|DISCRETE|BINNED|ORDINAL|IDENTITY)\s+)?(x|y|xmin|xmax|ymin|ymax|xend|yend|color|colour|fill|stroke|opacity|size|shape|linetype|linewidth|panel|row|column)\b/gi;
159+
160+
const POSITION_RE =
161+
/\bposition\s*=>\s*'(stack|dodge|jitter|identity)'/gi;
162+
163+
// ---------------------------------------------------------------------------
164+
// Line scanning
165+
// ---------------------------------------------------------------------------
166+
167+
/**
168+
* Find all linkable keyword matches in a single line of text.
169+
* Returns matches with 1-based column positions.
170+
*/
171+
function findLinksInLine(lineText: string): LinkMatch[] {
172+
const matches: LinkMatch[] = [];
173+
174+
// Helper: run a regex, look up the capture group in a map, push match
175+
function scan(
176+
re: RegExp,
177+
map: Record<string, DocLink>,
178+
captureGroup: number = 0,
179+
) {
180+
re.lastIndex = 0;
181+
let m: RegExpExecArray | null;
182+
while ((m = re.exec(lineText)) !== null) {
183+
const word = m[captureGroup];
184+
const link = map[word.toLowerCase()];
185+
if (!link) continue;
186+
187+
// The capture group is always the last token in our patterns
188+
const startOffset =
189+
captureGroup === 0
190+
? m.index
191+
: m.index + m[0].length - word.length;
192+
193+
matches.push({
194+
startCol: startOffset + 1, // 1-based
195+
endCol: startOffset + word.length + 1,
196+
link,
197+
});
198+
}
199+
}
200+
201+
// 1. Clause keywords (full match)
202+
scan(CLAUSE_RE, CLAUSE_LINKS, 1);
203+
204+
// 2. Geom types (capture group 1, after DRAW/PLACE)
205+
scan(GEOM_RE, GEOM_LINKS, 1);
206+
207+
// 3. Coord types (capture group 1, after TO)
208+
scan(COORD_RE, COORD_LINKS, 1);
209+
210+
// 4. Scale modifiers (capture group 1, after SCALE)
211+
scan(SCALE_TYPE_RE, SCALE_TYPE_LINKS, 1);
212+
213+
// 5. Aesthetics after AS (capture group 1)
214+
scan(AESTHETIC_AFTER_AS_RE, AESTHETIC_LINKS, 1);
215+
216+
// 6. Aesthetics after SCALE [modifier?] (capture group 1)
217+
scan(AESTHETIC_AFTER_SCALE_RE, AESTHETIC_LINKS, 1);
218+
219+
// 7. Position adjustments (capture group 1, inside quotes)
220+
scan(POSITION_RE, POSITION_LINKS, 1);
221+
222+
return matches;
223+
}
224+
225+
// ---------------------------------------------------------------------------
226+
// Registration
227+
// ---------------------------------------------------------------------------
228+
229+
let registered = false;
230+
231+
export function registerGgsqlLinks(siteRoot: string): void {
232+
if (registered) return;
233+
registered = true;
234+
235+
// Normalize siteRoot to end with /
236+
if (!siteRoot.endsWith("/")) siteRoot += "/";
237+
238+
// Resolve site root to an absolute URL for Monaco's opener
239+
const baseUrl = new URL(siteRoot, window.location.href).href;
240+
241+
// --- Link Provider ---
242+
monaco.languages.registerLinkProvider("ggsql", {
243+
provideLinks(model) {
244+
const links: monaco.languages.ILink[] = [];
245+
const lineCount = model.getLineCount();
246+
247+
for (let lineNum = 1; lineNum <= lineCount; lineNum++) {
248+
const lineText = model.getLineContent(lineNum);
249+
const lineMatches = findLinksInLine(lineText);
250+
251+
for (const lm of lineMatches) {
252+
links.push({
253+
range: new monaco.Range(
254+
lineNum,
255+
lm.startCol,
256+
lineNum,
257+
lm.endCol,
258+
),
259+
url: new URL(lm.link.url + ".html", baseUrl).href,
260+
tooltip: lm.link.label,
261+
});
262+
}
263+
}
264+
265+
return { links };
266+
},
267+
});
268+
269+
}

ggsql-wasm/demo/src/quarto/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ async function applyEditors(
222222

223223
cell.codeScaffold.replaceWith(wrapper);
224224

225-
const editorInst = await createEditor(editorContainer, cell.query);
225+
const editorInst = await createEditor(editorContainer, cell.query, SITE_ROOT);
226226
cell.editor = editorInst;
227227

228228
if (cell.result && cell.visId && cell.visContainer) {

0 commit comments

Comments
 (0)