Skip to content

Commit 81ec301

Browse files
committed
fix: #436 improve SQL keyword suggestions
1 parent 1e853da commit 81ec301

6 files changed

Lines changed: 318 additions & 9 deletions

File tree

README-zh_CN.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,18 @@ console.log(sqlSlices)
262262
[ 'CATALOG', 'FUNCTION', 'TEMPORARY', 'VIEW', 'DATABASE', 'TABLE' ]
263263
*/
264264
```
265+
266+
可以通过可选的 `keywordFilter` 移除不需要的关键字候选项。返回 `true` 保留关键字,返回 `false` 移除关键字。
267+
268+
```typescript
269+
const sql = 'SELECT * FROM tb ';
270+
const pos = { lineNumber: 1, column: sql.length + 1 };
271+
const excludedKeywords = new Set(['WHERE', 'ORDER BY']);
272+
const keywords = flink.getSuggestionAtCaretPosition(sql, pos, {
273+
keywordFilter: (keyword) => !excludedKeywords.has(keyword),
274+
})?.keywords;
275+
```
276+
265277
+ **获取语法相关自动补全信息**
266278
```typescript
267279
import { FlinkSQL } from 'dt-sql-parser';

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,18 @@ Call the `getAllEntities` method on the SQL instance, pass the SQL content and t
263263
[ 'CATALOG', 'FUNCTION', 'TEMPORARY', 'VIEW', 'DATABASE', 'TABLE' ]
264264
*/
265265
```
266+
267+
Use the optional `keywordFilter` to remove unwanted keyword candidates. Return `true` to keep a keyword and `false` to remove it.
268+
269+
```typescript
270+
const sql = 'SELECT * FROM tb ';
271+
const pos = { lineNumber: 1, column: sql.length + 1 };
272+
const excludedKeywords = new Set(['WHERE', 'ORDER BY']);
273+
const keywords = flink.getSuggestionAtCaretPosition(sql, pos, {
274+
keywordFilter: (keyword) => !excludedKeywords.has(keyword),
275+
})?.keywords;
276+
```
277+
266278
+ **Obtaining information related to grammar completion**
267279
```javascript
268280
import { FlinkSQL } from 'dt-sql-parser';

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ export { EntityContextType } from './parser/common/types';
3232

3333
export { StmtContextType } from './parser/common/entityCollector';
3434

35-
export type { CaretPosition, Suggestions, SyntaxSuggestion } from './parser/common/types';
35+
export type {
36+
CaretPosition,
37+
SuggestionOptions,
38+
Suggestions,
39+
SyntaxSuggestion,
40+
} from './parser/common/types';
3641

3742
export type { WordRange, TextSlice } from './parser/common/textAndWord';
3843

src/parser/common/basicSQL.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
CaretPosition,
2727
LOCALE_TYPE,
2828
SemanticCollectOptions,
29+
SuggestionOptions,
2930
Suggestions,
3031
SyntaxSuggestion,
3132
} from './types';
@@ -48,6 +49,7 @@ export abstract class BasicSQL<
4849
protected _parseTree: PRC | null;
4950
protected _parsedInput: string;
5051
protected _parseErrors: ParseError[] = [];
52+
private _statementStartTokenTypes: Set<number> | null = null;
5153
/** members for cache end */
5254

5355
private _errorListener: ErrorListener = (error) => {
@@ -555,15 +557,95 @@ export abstract class BasicSQL<
555557
};
556558
}
557559

560+
/**
561+
* Get the minimum statement tree for collecting completion candidates
562+
*/
563+
private getSuggestionParseTree(
564+
parseTree: ParserRuleContext,
565+
caretTokenIndex: number
566+
): ParserRuleContext {
567+
const children = parseTree.children;
568+
if (!children?.length) return parseTree;
569+
570+
for (let index = children.length - 1; index >= 0; index--) {
571+
const child = children[index];
572+
if (!(child instanceof ParserRuleContext)) continue;
573+
574+
const startTokenIndex = child.start?.tokenIndex;
575+
const stopTokenIndex = child.stop?.tokenIndex;
576+
if (
577+
startTokenIndex === undefined ||
578+
stopTokenIndex === undefined ||
579+
startTokenIndex > caretTokenIndex
580+
)
581+
continue;
582+
583+
// Use the current statement tree when the caret is inside it
584+
if (stopTokenIndex >= caretTokenIndex) return child;
585+
586+
// Keep using the current statement until it ends with a semicolon
587+
return child.stop?.text === SQL_SPLIT_SYMBOL_TEXT ? parseTree : child;
588+
}
589+
590+
return parseTree;
591+
}
592+
593+
/**
594+
* Collect candidates for the current statement and remove new-statement-only keywords
595+
*/
596+
private collectSuggestionCandidates(
597+
parser: Parser,
598+
parseTree: ParserRuleContext,
599+
caretTokenIndex: number
600+
): CandidatesCollection {
601+
const core = new CodeCompletionCore(parser);
602+
core.preferredRules = this.preferredRules;
603+
const candidates = core.collectCandidates(caretTokenIndex, parseTree);
604+
const suggestionParseTree = this.getSuggestionParseTree(parseTree, caretTokenIndex);
605+
606+
if (suggestionParseTree === parseTree) return candidates;
607+
608+
const statementCore = new CodeCompletionCore(parser);
609+
statementCore.preferredRules = this.preferredRules;
610+
const statementCandidates = statementCore.collectCandidates(
611+
caretTokenIndex,
612+
suggestionParseTree
613+
);
614+
615+
if (this._statementStartTokenTypes === null) {
616+
const statementStartCore = new CodeCompletionCore(parser);
617+
statementStartCore.preferredRules = this.preferredRules;
618+
const statementStartCandidates = statementStartCore.collectCandidates(0, parseTree);
619+
this._statementStartTokenTypes = new Set(statementStartCandidates.tokens.keys());
620+
}
621+
622+
const tokens = new Map(candidates.tokens);
623+
for (const tokenType of this._statementStartTokenTypes) {
624+
if (!statementCandidates.tokens.has(tokenType)) {
625+
tokens.delete(tokenType);
626+
} else if (tokens.has(tokenType)) {
627+
// Use the current statement follow-list to preserve valid combined keywords
628+
tokens.set(tokenType, statementCandidates.tokens.get(tokenType)!);
629+
}
630+
}
631+
632+
return {
633+
rules: candidates.rules,
634+
tokens,
635+
};
636+
}
637+
558638
/**
559639
* Get suggestions of syntax and token at caretPosition
560640
* @param input source string
561641
* @param caretPosition caret position, such as cursor position
642+
* @param options suggestion options
562643
* @returns suggestion
563644
*/
564645
public getSuggestionAtCaretPosition(
565646
input: string,
566-
caretPosition: CaretPosition
647+
caretPosition: CaretPosition,
648+
options?: SuggestionOptions
567649
): Suggestions | null {
568650
this.parseWithCache(input);
569651
if (!this._parseTree) return null;
@@ -614,12 +696,11 @@ export abstract class BasicSQL<
614696
parseTree = sqlParserIns.program();
615697
}
616698

617-
const core = new CodeCompletionCore(sqlParserIns);
618-
core.preferredRules = this.preferredRules;
619-
// core.showRuleStack = true;
620-
// core.showResult = true;
621-
622-
const candidates = core.collectCandidates(caretTokenIndex, parseTree);
699+
const candidates = this.collectSuggestionCandidates(
700+
sqlParserIns,
701+
parseTree,
702+
caretTokenIndex
703+
);
623704
const originalSuggestions = this.processCandidates(candidates, allTokens, caretTokenIndex);
624705

625706
const syntaxSuggestions: SyntaxSuggestion<WordRange>[] = originalSuggestions.syntax.map(
@@ -633,9 +714,14 @@ export abstract class BasicSQL<
633714
};
634715
}
635716
);
717+
const keywordFilter = options?.keywordFilter;
718+
const keywords = keywordFilter
719+
? originalSuggestions.keywords.filter((keyword) => keywordFilter(keyword))
720+
: originalSuggestions.keywords;
721+
636722
return {
637723
syntax: syntaxSuggestions,
638-
keywords: originalSuggestions.keywords,
724+
keywords,
639725
};
640726
}
641727

src/parser/common/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ export interface Suggestions<T = WordRange> {
7474
readonly keywords: string[];
7575
}
7676

77+
/**
78+
* Suggested information options
79+
*/
80+
export interface SuggestionOptions {
81+
/**
82+
* Return true to keep the keyword, otherwise remove it
83+
*/
84+
keywordFilter?: (keyword: string) => boolean;
85+
}
86+
7787
export type LOCALE_TYPE = 'zh_CN' | 'en_US';
7888

7989
export interface SemanticContext {

0 commit comments

Comments
 (0)