Merge pull request #118 from agnosticeng/feat/go-to-def
This commit is contained in:
37
package-lock.json
generated
37
package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^15.0.8",
|
||||
"marked-highlight": "^2.2.1",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
@@ -45,7 +46,8 @@
|
||||
"svelte-check": "^4.1.5",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.2.6"
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-devtools-json": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@agnosticeng/cache": {
|
||||
@@ -2318,6 +2320,12 @@
|
||||
"marked": ">=4 <16"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.52.2",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||
@@ -2727,6 +2735,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
|
||||
@@ -2799,6 +2821,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-devtools-json": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-0.1.0.tgz",
|
||||
"integrity": "sha512-KvdgPBUAAhwnpOgXmJhs6KI4/IPn6xUppLGm20D0Uvp/doZ9TpTYYfzSHX0TgvdsSlTAwzZfzDse+ujGskp68g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.5.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^15.0.8",
|
||||
"marked-highlight": "^2.2.1",
|
||||
"mitt": "^3.0.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
@@ -51,6 +52,7 @@
|
||||
"svelte-check": "^4.1.5",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.2.6"
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-devtools-json": "^0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<script lang="ts">
|
||||
let { columns } = $props();
|
||||
import type { ColumnDescriptor } from '$lib/olap-engine';
|
||||
|
||||
interface Props {
|
||||
columns: ColumnDescriptor[];
|
||||
}
|
||||
|
||||
let { columns }: Props = $props();
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
|
||||
@@ -3,18 +3,34 @@
|
||||
import FolderOpen from '$lib/icons/FolderOpen.svelte';
|
||||
import Table from '$lib/icons/Table.svelte';
|
||||
import Columns from './Columns.svelte';
|
||||
import { onExpand } from './emitter';
|
||||
import Tree from './Tree.svelte';
|
||||
import { findNodeInTree, type TreeNode } from './utils';
|
||||
|
||||
interface Props {
|
||||
node: TreeNode;
|
||||
level?: number;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
let { node, level = 0, expanded: forceExpanded }: Props = $props();
|
||||
|
||||
let { node = {}, level = 0, expanded: forceExpanded = false } = $props();
|
||||
let expanded = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
expanded = forceExpanded;
|
||||
if (typeof forceExpanded === 'boolean') expanded = forceExpanded;
|
||||
});
|
||||
|
||||
function toggleExpanded() {
|
||||
expanded = !expanded;
|
||||
}
|
||||
|
||||
$effect(() =>
|
||||
onExpand((value) => {
|
||||
if (node.type === 'dataset' && node.value === value) expanded = true;
|
||||
else if (node.type === 'group' && findNodeInTree(node.children, value)) expanded = true;
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="node" style:opacity={expanded ? 1 : 0.7 + level}>
|
||||
@@ -43,10 +59,12 @@
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{#if node.type === 'group' && node.children && expanded}
|
||||
{#each node.children as child}
|
||||
<Tree node={child} level={level + 1} expanded={forceExpanded} />
|
||||
{/each}
|
||||
{#if node.type === 'group'}
|
||||
<div style:display={expanded ? 'contents' : 'none'}>
|
||||
{#each node.children as child}
|
||||
<Tree node={child} level={level + 1} expanded={forceExpanded} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if node.type === 'dataset' && expanded}
|
||||
<div class="dataset">
|
||||
|
||||
18
src/lib/components/Datasets/emitter.ts
Normal file
18
src/lib/components/Datasets/emitter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
import mitt from 'mitt';
|
||||
|
||||
type Events = {
|
||||
expand: Table['name'];
|
||||
};
|
||||
|
||||
const emitter = mitt<Events>();
|
||||
|
||||
export function goToDefinition(tableName: string) {
|
||||
emitter.emit('expand', tableName);
|
||||
}
|
||||
|
||||
export function onExpand(handler: (tableName: string) => void) {
|
||||
emitter.on('expand', handler);
|
||||
|
||||
return () => emitter.off('expand', handler);
|
||||
}
|
||||
2
src/lib/components/Datasets/index.ts
Normal file
2
src/lib/components/Datasets/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './Datasets.svelte';
|
||||
export * from './emitter';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type ColumnDescriptor, type Table } from '$lib/olap-engine';
|
||||
import type { ColumnDescriptor, Table } from '$lib/olap-engine';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function filter(sources: Table[], search: string) {
|
||||
@@ -12,12 +12,19 @@ export function filter(sources: Table[], search: string) {
|
||||
);
|
||||
}
|
||||
|
||||
type TreeNode = {
|
||||
export type TreeNode = GroupTreeNode | DatasetTreeNode;
|
||||
|
||||
type GroupTreeNode = {
|
||||
name: string;
|
||||
type: 'group' | 'dataset';
|
||||
value?: string;
|
||||
columns?: ColumnDescriptor[];
|
||||
children?: TreeNode[];
|
||||
type: 'group';
|
||||
children: TreeNode[];
|
||||
};
|
||||
|
||||
type DatasetTreeNode = {
|
||||
name: string;
|
||||
type: 'dataset';
|
||||
value: string;
|
||||
columns: ColumnDescriptor[];
|
||||
};
|
||||
|
||||
export function buildTree(tables: Table[]): TreeNode[] {
|
||||
@@ -36,14 +43,28 @@ export function buildTree(tables: Table[]): TreeNode[] {
|
||||
const toArray = (obj: any): TreeNode[] =>
|
||||
_.map(
|
||||
obj,
|
||||
(value, key): TreeNode => ({
|
||||
name: key,
|
||||
type: value.children ? 'group' : 'dataset',
|
||||
value: value.value,
|
||||
columns: value.columns,
|
||||
children: value.children ? toArray(value.children) : undefined
|
||||
})
|
||||
(value, key) =>
|
||||
({
|
||||
name: key,
|
||||
type: value.children ? 'group' : 'dataset',
|
||||
value: value.value,
|
||||
columns: value.columns,
|
||||
children: value.children ? toArray(value.children) : undefined
|
||||
}) as TreeNode
|
||||
);
|
||||
|
||||
return toArray(root);
|
||||
}
|
||||
|
||||
export function findNodeInTree(
|
||||
tree: TreeNode[],
|
||||
value: DatasetTreeNode['value']
|
||||
): TreeNode | undefined {
|
||||
for (const node of tree) {
|
||||
if (node.type === 'dataset' && node.value === value) return node;
|
||||
else if (node.type === 'group') {
|
||||
const found = findNodeInTree(node.children!, value);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<script lang="ts">
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
import './theme';
|
||||
import { onDestroy } from 'svelte';
|
||||
import './language';
|
||||
import './theme';
|
||||
|
||||
let { value = $bindable() } = $props();
|
||||
interface Props {
|
||||
value?: string;
|
||||
onCmdClick?: (word: string) => void;
|
||||
linkables?: string[];
|
||||
}
|
||||
|
||||
let { value = $bindable(''), onCmdClick, linkables }: Props = $props();
|
||||
let editorElement: HTMLElement;
|
||||
let editor: any;
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
|
||||
function initMonaco() {
|
||||
if (editorElement) {
|
||||
@@ -21,9 +28,7 @@
|
||||
},
|
||||
quickSuggestions: true,
|
||||
suggestOnTriggerCharacters: true,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'on',
|
||||
lineNumbersMinChars: 4,
|
||||
lineDecorationsWidth: 5,
|
||||
@@ -40,6 +45,63 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
editor.onMouseDown((e) => {
|
||||
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
|
||||
const isMetaKey = isMac ? e.event.metaKey : e.event.ctrlKey;
|
||||
|
||||
if (isMetaKey && e.target.type === monaco.editor.MouseTargetType.CONTENT_TEXT) {
|
||||
const position = e.target.position;
|
||||
const model = editor.getModel();
|
||||
const word = model?.getWordAtPosition(position);
|
||||
|
||||
if (word) {
|
||||
const range = new monaco.Range(
|
||||
position.lineNumber,
|
||||
word.startColumn,
|
||||
position.lineNumber,
|
||||
word.endColumn
|
||||
);
|
||||
const text = model!.getValueInRange(range);
|
||||
if (linkables?.includes(text)) {
|
||||
onCmdClick?.(text);
|
||||
editor.setSelection(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let ids: string[] = [];
|
||||
|
||||
editor.onMouseMove((e) => {
|
||||
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
|
||||
const isMetaKey = isMac ? e.event.metaKey : e.event.ctrlKey;
|
||||
if (e.target.type !== monaco.editor.MouseTargetType.CONTENT_TEXT) return;
|
||||
const model = editor.getModel();
|
||||
if (!model) return;
|
||||
const position = e.target.position;
|
||||
const word = model.getWordAtPosition(position);
|
||||
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
|
||||
|
||||
if (word && isMetaKey && linkables?.includes(word.word)) {
|
||||
const range = new monaco.Range(
|
||||
position.lineNumber,
|
||||
word.startColumn,
|
||||
position.lineNumber,
|
||||
word.endColumn
|
||||
);
|
||||
|
||||
decorations.push({
|
||||
options: {
|
||||
inlineClassName: 'to-go-def',
|
||||
hoverMessage: { value: 'Command+Click to go to definition' }
|
||||
},
|
||||
range
|
||||
});
|
||||
}
|
||||
|
||||
ids = model.deltaDecorations(ids, decorations);
|
||||
});
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
value = editor.getValue();
|
||||
});
|
||||
@@ -57,6 +119,10 @@
|
||||
editor.setValue(value);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) editor.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={editorElement}></div>
|
||||
@@ -65,5 +131,11 @@
|
||||
div {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& :global(.to-go-def) {
|
||||
color: #007acc;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,31 @@ export function setupLanguage(
|
||||
columns: string[] = []
|
||||
) {
|
||||
monaco.languages.register({ id });
|
||||
monaco.languages.setLanguageConfiguration(id, {
|
||||
comments: {
|
||||
lineComment: '--',
|
||||
blockComment: ['/*', '*/']
|
||||
},
|
||||
brackets: [
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')']
|
||||
],
|
||||
autoClosingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" }
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: '{', close: '}' },
|
||||
{ open: '[', close: ']' },
|
||||
{ open: '(', close: ')' },
|
||||
{ open: '"', close: '"' },
|
||||
{ open: "'", close: "'" }
|
||||
]
|
||||
});
|
||||
monaco.languages.setMonarchTokensProvider(id, {
|
||||
ignoreCase: true,
|
||||
keywords: keywords.map((k) => k.split(' ')[0]),
|
||||
@@ -19,6 +44,11 @@ export function setupLanguage(
|
||||
operators: operators,
|
||||
columns: columns,
|
||||
|
||||
brackets: [
|
||||
{ open: '[', close: ']', token: 'delimiter.square' },
|
||||
{ open: '(', close: ')', token: 'delimiter.parenthesis' }
|
||||
],
|
||||
|
||||
symbols: /[=><!~?:&|+\-*\/\^%]+/,
|
||||
|
||||
tokenizer: {
|
||||
@@ -65,7 +95,7 @@ export function setupLanguage(
|
||||
}
|
||||
});
|
||||
|
||||
const sql = {
|
||||
const sql: monaco.languages.CompletionItemProvider = {
|
||||
provideCompletionItems: (model: monaco.editor.ITextModel, position: monaco.Position) => {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import type { Table } from '$lib/olap-engine';
|
||||
import type { HistoryEntry } from '$lib/repositories/history';
|
||||
import type { Query } from '$lib/repositories/queries';
|
||||
import Datasets from './Datasets/Datasets.svelte';
|
||||
import { tick } from 'svelte';
|
||||
import Datasets, { goToDefinition, onExpand as onDatasetExpand } from './Datasets';
|
||||
import History from './History.svelte';
|
||||
import ProxySwitch from './ProxySwitch.svelte';
|
||||
import Queries from './Queries/Queries.svelte';
|
||||
@@ -14,6 +15,16 @@
|
||||
tab = next;
|
||||
}
|
||||
|
||||
$effect(() =>
|
||||
onDatasetExpand(async (table) => {
|
||||
if (tab !== 'sources') {
|
||||
tab = 'sources';
|
||||
await tick();
|
||||
goToDefinition(table);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
type Props = {
|
||||
tables?: Table[];
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { Log } from '$lib/components/Console.svelte';
|
||||
import { ContextMenuState } from '$lib/components/ContextMenu';
|
||||
import ContextMenu from '$lib/components/ContextMenu/ContextMenu.svelte';
|
||||
import { goToDefinition } from '$lib/components/Datasets';
|
||||
import Drawer from '$lib/components/Drawer.svelte';
|
||||
import { functions, keywords, operators, types } from '$lib/components/Editor/clickhouse';
|
||||
import Editor from '$lib/components/Editor/Editor.svelte';
|
||||
@@ -550,7 +551,11 @@ LIMIT 100;`;
|
||||
</nav>
|
||||
{#each tabs as tab, i (tab.id)}
|
||||
<div style:display={selectedTabIndex == i ? 'block' : 'none'}>
|
||||
<Editor bind:value={tab.content} />
|
||||
<Editor
|
||||
bind:value={tab.content}
|
||||
linkables={tables.map((t) => t.name)}
|
||||
onCmdClick={goToDefinition}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import devtoolsJson from 'vite-plugin-devtools-json';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [sveltekit()],
|
||||
plugins: [sveltekit(), devtoolsJson()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user