Skip to content

Commit cde1c44

Browse files
committed
Flowquery APP
1 parent 1069951 commit cde1c44

File tree

5 files changed

+500
-14
lines changed

5 files changed

+500
-14
lines changed

flowquery-app/package-lock.json

Lines changed: 152 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flowquery-app/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@
77
"build": "vite build"
88
},
99
"dependencies": {
10+
"@codemirror/autocomplete": "^6.20.1",
11+
"@codemirror/commands": "^6.10.2",
12+
"@codemirror/language": "^6.12.2",
13+
"@codemirror/search": "^6.6.0",
14+
"@codemirror/state": "^6.5.4",
15+
"@codemirror/view": "^6.39.16",
1016
"@fluentui/react-components": "^9.56.0",
1117
"@fluentui/react-icons": "^2.0.320",
18+
"@lezer/highlight": "^1.2.3",
19+
"codemirror": "^6.0.2",
1220
"react": "^18.3.0",
1321
"react-dom": "^18.3.0"
1422
},

flowquery-app/src/App.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import {
22
FluentProvider,
33
Input,
44
Text,
5-
Textarea,
65
Toolbar,
76
ToolbarButton,
87
webLightTheme,
98
} from "@fluentui/react-components";
109
import React from "react";
1110
import { Compression } from "./compression";
11+
import { CypherEditor } from "./CypherEditor";
1212
import { ResultsTable } from "./ResultsTable";
1313

1414
function formatMetadataKey(key: string): string {
@@ -81,13 +81,6 @@ export class App extends React.Component<Record<string, never>, AppState> {
8181
this.setState({ input: "", results: [], metadata: null, error: null, shareLink: "" });
8282
};
8383

84-
handleKeyDown = (e: React.KeyboardEvent) => {
85-
if (e.key === "Enter" && e.shiftKey) {
86-
e.preventDefault();
87-
this.run();
88-
}
89-
};
90-
9184
render() {
9285
const { input, results, metadata, error, shareLink } = this.state;
9386

@@ -102,14 +95,11 @@ export class App extends React.Component<Record<string, never>, AppState> {
10295
boxSizing: "border-box",
10396
}}
10497
>
105-
<Textarea
98+
<CypherEditor
10699
value={input}
107-
onChange={(_, d) => this.setState({ input: d.value })}
108-
onKeyDown={this.handleKeyDown}
100+
onChange={(v) => this.setState({ input: v })}
101+
onShiftEnter={this.run}
109102
placeholder="Type your FlowQuery statement here and press Shift+Enter to run it."
110-
resize="vertical"
111-
textarea={{ style: { fontFamily: "monospace", minHeight: 200 } }}
112-
style={{ width: "100%" }}
113103
/>
114104
<Toolbar style={{ padding: "8px 0", gap: 4 }}>
115105
<ToolbarButton appearance="primary" onClick={this.run}>

flowquery-app/src/CypherEditor.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React, { useEffect, useRef } from "react";
2+
import { EditorState } from "@codemirror/state";
3+
import { EditorView, keymap, placeholder as cmPlaceholder } from "@codemirror/view";
4+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
5+
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from "@codemirror/language";
6+
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
7+
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
8+
import { cypherLanguage } from "./cypher-lang";
9+
10+
interface CypherEditorProps {
11+
value: string;
12+
onChange: (value: string) => void;
13+
onShiftEnter?: () => void;
14+
placeholder?: string;
15+
}
16+
17+
export const CypherEditor: React.FC<CypherEditorProps> = ({
18+
value,
19+
onChange,
20+
onShiftEnter,
21+
placeholder,
22+
}) => {
23+
const containerRef = useRef<HTMLDivElement>(null);
24+
const viewRef = useRef<EditorView | null>(null);
25+
const onChangeRef = useRef(onChange);
26+
const onShiftEnterRef = useRef(onShiftEnter);
27+
28+
onChangeRef.current = onChange;
29+
onShiftEnterRef.current = onShiftEnter;
30+
31+
useEffect(() => {
32+
if (!containerRef.current) return;
33+
34+
const shiftEnterKeymap = keymap.of([
35+
{
36+
key: "Shift-Enter",
37+
run: () => {
38+
onShiftEnterRef.current?.();
39+
return true;
40+
},
41+
},
42+
]);
43+
44+
const updateListener = EditorView.updateListener.of((update) => {
45+
if (update.docChanged) {
46+
onChangeRef.current(update.state.doc.toString());
47+
}
48+
});
49+
50+
const theme = EditorView.theme({
51+
"&": {
52+
fontFamily: "monospace",
53+
fontSize: "14px",
54+
border: "1px solid #d1d1d1",
55+
borderRadius: "4px",
56+
minHeight: "120px",
57+
maxHeight: "40vh",
58+
},
59+
"&.cm-focused": {
60+
outline: "2px solid #0078d4",
61+
outlineOffset: "-1px",
62+
},
63+
".cm-content": {
64+
padding: "8px 12px",
65+
},
66+
".cm-scroller": {
67+
overflow: "auto",
68+
},
69+
});
70+
71+
const state = EditorState.create({
72+
doc: value,
73+
extensions: [
74+
shiftEnterKeymap,
75+
history(),
76+
closeBrackets(),
77+
bracketMatching(),
78+
highlightSelectionMatches(),
79+
cypherLanguage,
80+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
81+
keymap.of([
82+
...closeBracketsKeymap,
83+
...defaultKeymap,
84+
...searchKeymap,
85+
...historyKeymap,
86+
]),
87+
updateListener,
88+
theme,
89+
...(placeholder ? [cmPlaceholder(placeholder)] : []),
90+
EditorView.lineWrapping,
91+
],
92+
});
93+
94+
const view = new EditorView({
95+
state,
96+
parent: containerRef.current,
97+
});
98+
99+
viewRef.current = view;
100+
101+
return () => {
102+
view.destroy();
103+
viewRef.current = null;
104+
};
105+
// eslint-disable-next-line react-hooks/exhaustive-deps
106+
}, []);
107+
108+
// Sync external value changes into the editor
109+
useEffect(() => {
110+
const view = viewRef.current;
111+
if (!view) return;
112+
const current = view.state.doc.toString();
113+
if (current !== value) {
114+
view.dispatch({
115+
changes: { from: 0, to: current.length, insert: value },
116+
});
117+
}
118+
}, [value]);
119+
120+
return <div ref={containerRef} style={{ width: "100%" }} />;
121+
};

0 commit comments

Comments
 (0)