Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add column hover tooltip and column value auto completion #1526

Merged
merged 3 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,13 @@ export const TableColumnTooltip: React.FunctionComponent<IProps> = ({
<div>{column.name}</div>
</div>
{column.type && (
<div className="mt4">
<div className="tooltip-title">Type</div>
<div className="mt4 flex-row">
<div className="tooltip-title">Type:</div>
<div className="tooltip-content">{column.type}</div>
</div>
)}
{tagsDOM.length > 0 && (
<div className="mt4">
<div className="tooltip-title">Tags</div>
<div className="tooltip-content">
<div className="DataTableTags flex-row">{tagsDOM}</div>
</div>
</div>
<div className="DataTableTags flex-row">{tagsDOM}</div>
)}
{column.comment && (
<div className="mt4">
Expand All @@ -75,7 +70,7 @@ export const TableColumnTooltip: React.FunctionComponent<IProps> = ({
<div className="tooltip-content">{description}</div>
</div>
)}
{!!column?.stats?.length && (
{statsDOM.length && (
<div className="mt4">
<div className="tooltip-title">Stats</div>
<div className="tooltip-content">{statsDOM}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import {
autocompletion,
Completion,
CompletionContext,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { EditorView } from '@uiw/react-codemirror';
import { useCallback, useEffect, useMemo, useState } from 'react';

import { CodeMirrorToken } from 'lib/codemirror/utils';
import { IPosition } from 'lib/sql-helper/sql-lexer';
import { SqlParser } from 'lib/sql-helper/sql-parser';

export type AutoCompleteType = 'none' | 'schema' | 'all';
Expand All @@ -26,8 +29,7 @@ export const useAutoCompleteExtension = ({
const [typing, setTyping] = useState(false);

const getColumnValueCompletions = useCallback(
(cursor, token) => {
console.log('getColumnValueCompletions', cursor, token);
(cursor: IPosition, token: CodeMirrorToken): CompletionResult => {
const [textBeforeEqual, textAfterEqual] = token.text
.split('=')
.map((s) => s.trim());
Expand All @@ -49,40 +51,15 @@ export const useAutoCompleteExtension = ({
[sqlParserRef]
);

const getCompletions = useCallback(
async (context: CompletionContext) => {
if (type === 'none') {
return null;
}

// Get the token before the cursor, token could be in below foramts
// - column: schema.table.column, table.column, column
// - table: schema.table, table
// - keyword: any keyword
// - column value: column = value, (value may be quoted)
const token = context.matchBefore(
/(\w+\.){0,2}\w*|(\w+.)?\s*=\s*'?\w*/
);

// no token before the cursor, don't open completions.
if (!token?.text) return null;

// Get the cursor position in codemirror v5 format
const cursorPos = context.pos;
const line = context.state.doc.lineAt(cursorPos);
const cursor = { line: line.number - 1, ch: cursorPos - line.from };

const getGeneralCompletions = useCallback(
async (
context: string,
cursor: IPosition,
token: CodeMirrorToken
): Promise<CompletionResult> => {
const tokenText = token.text.toLowerCase();
const sqlParserContext =
sqlParserRef.current.getContextAtPos(cursor);

// handle the case where the token is a column and the user is trying to type a value in a where clause
if (sqlParserContext === 'column' && tokenText.includes('=')) {
return getColumnValueCompletions(cursor, token);
}

const options: Completion[] = [];
if (sqlParserContext === 'column') {
if (context === 'column') {
const columns = sqlParserRef.current.getColumnMatches(
cursor,
token.text
Expand All @@ -93,7 +70,7 @@ export const useAutoCompleteExtension = ({
detail: 'column',
}))
);
} else if (sqlParserContext === 'table') {
} else if (context === 'table') {
const tableNames =
await sqlParserRef.current.getTableNameMatches(tokenText);
options.push(
Expand All @@ -118,13 +95,56 @@ export const useAutoCompleteExtension = ({
);

let from = token.from;
if (sqlParserContext === 'column') {
if (context === 'column') {
from += token.text.lastIndexOf('.') + 1;
}

return { from, options };
},
[sqlParserRef, type, getColumnValueCompletions]
[sqlParserRef, type]
);

const getCompletions = useCallback(
async (context: CompletionContext) => {
if (type === 'none') {
return null;
}

// Get the token before the cursor, token could be in below foramts
// - column value: column = value (value may be quoted)
const columnValueRegex = /(\w+\.)?\w+\s*=\s*['"]?\w*/;
// - column: schema.table.column, table.column, column
// - table: schema.table, table
// - keyword: any keyword
const generalTokenRegex = /(\w+\.){0,2}\w*/;

const columnValueToken = context.matchBefore(columnValueRegex);
const generalToken =
!columnValueToken && context.matchBefore(generalTokenRegex);

// no token before the cursor, don't open completions.
if (!columnValueToken?.text && !generalToken.text) return null;
jczhong84 marked this conversation as resolved.
Show resolved Hide resolved

// Get the cursor position in codemirror v5 format
const cursorPos = context.pos;
const line = context.state.doc.lineAt(cursorPos);
const cursor = { line: line.number - 1, ch: cursorPos - line.from };

const sqlParserContext =
sqlParserRef.current.getContextAtPos(cursor);

// handle the case where the token is a column and the user is trying to type a value in a where clause
if (sqlParserContext === 'column' && columnValueToken?.text) {
return getColumnValueCompletions(cursor, columnValueToken);
}

return getGeneralCompletions(
sqlParserContext,
cursor,
generalToken
);
},
[sqlParserRef, type, getColumnValueCompletions, getGeneralCompletions]
);

const triggerCompletionOnType = () => {
Expand Down
36 changes: 15 additions & 21 deletions querybook/webapp/lib/sql-helper/sql-parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { bind } from 'lodash-decorators';

import { IDataColumn } from 'const/metastore';
import { ICodeAnalysis, TableToken } from 'lib/sql-helper/sql-lexer';
import { ICodeAnalysis, IPosition, TableToken } from 'lib/sql-helper/sql-lexer';
import {
getLanguageSetting,
ILanguageSetting,
Expand Down Expand Up @@ -59,7 +59,7 @@ export class SqlParser {
* @param pos position of the cursor or mouse pointer
* @returns string: 'table', 'column' or 'none'
*/
public getContextAtPos(pos: { line: number; ch: number }): string {
public getContextAtPos(pos: IPosition): string {
if (!this.codeAnalysis?.editorLines) {
return 'none';
}
Expand All @@ -77,7 +77,7 @@ export class SqlParser {
* @param pos position of the cursor or mouse pointer
* @returns TableToken if the cursor is on a table, otherwise null
*/
public getTableAtPos(pos: { line: number; ch: number }): TableToken | null {
public getTableAtPos(pos: IPosition): TableToken | null {
const { line, ch } = pos;
if (this.codeAnalysis) {
const tableReferences: TableToken[] = [].concat.apply(
Expand Down Expand Up @@ -111,10 +111,7 @@ export class SqlParser {
* @param text the token text before or at the cursor
* @returns IDataColumn if the cursor is on a column, otherwise null
*/
public getColumnAtPos(
pos: { line: number; ch: number },
text: string
): IDataColumn | null {
public getColumnAtPos(pos: IPosition, text: string): IDataColumn | null {
const columns = this.getColumnMatches(pos, text, true);
if (columns.length === 1) {
return columns[0];
Expand All @@ -129,23 +126,20 @@ export class SqlParser {
* @returns Array of column values if the cursor is on a column, otherwise empty array
*/
public getColumnValues(
cursor: { line: number; ch: number },
cursor: IPosition,
text: string
): Array<number | string> {
const columns = this.getColumnMatches(cursor, text, true);

if (columns.length === 1) {
const colStats = columns[0].stats ?? [];
if (columns.length !== 1) return [];

// find the stat with key="distinct_values"
const distinctValuesStat = colStats.find(
(stat) => stat.key === 'distinct_values'
);
if (distinctValuesStat?.value instanceof Array) {
return distinctValuesStat?.value;
}

return [];
const colStats = columns[0].stats ?? [];
// find the stat with key="distinct_values"
const distinctValuesStat = colStats.find(
(stat) => stat.key === 'distinct_values'
);
if (distinctValuesStat?.value instanceof Array) {
return distinctValuesStat?.value;
}

return [];
Expand All @@ -159,7 +153,7 @@ export class SqlParser {
* @param exactMatch whether to do exact match or prefix match
*/
public getColumnMatches(
cursor: { line: number; ch: number },
cursor: IPosition,
text: string,
exactMatch: boolean = false
): IDataColumn[] {
Expand Down Expand Up @@ -255,7 +249,7 @@ export class SqlParser {
});
}

private getLineAnalysis(cursor: { line: number; ch: number }) {
private getLineAnalysis(cursor: IPosition): ILineAnalysis {
const lineAnalysis: ILineAnalysis = {
context: 'none',
alias: {},
Expand Down
Loading