Merge pull request #118 from agnosticeng/feat/go-to-def

This commit is contained in:
Yann Amsellem
2025-05-13 17:28:13 +02:00
committed by GitHub
12 changed files with 253 additions and 32 deletions

37
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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">

View 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);
}

View File

@@ -0,0 +1,2 @@
export { default } from './Datasets.svelte';
export * from './emitter';

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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 = {

View File

@@ -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[];

View File

@@ -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>

View File

@@ -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`
//