Skip to content

Commit 4b5d34b

Browse files
committed
fix: #165 disable completion in comments
1 parent f5378e1 commit 4b5d34b

3 files changed

Lines changed: 181 additions & 0 deletions

File tree

src/languageFeatures.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { WordPosition } from 'dt-sql-parser/dist/parser/common/textAndWord';
55
import * as monaco from 'monaco-editor';
66

77
import { BaseSQLWorker } from './baseSQLWorker';
8+
import { TokenClassConsts } from './common/constants';
89
import { debounce } from './common/utils';
910
import {
1011
CancellationToken,
@@ -145,6 +146,25 @@ function toDiagnostics(_resource: Uri, diag: ParseError): editor.IMarkerData {
145146
};
146147
}
147148

149+
function isPositionInComment(model: editor.IReadOnlyModel, position: Position): boolean {
150+
const textBeforePosition = model.getValueInRange(
151+
new Range(1, 1, position.lineNumber, position.column)
152+
);
153+
// 在光标处追加哨兵字符,避免将刚结束的块注释误判为仍在注释中
154+
const tokenizedLines = editor.tokenize(`${textBeforePosition}x`, model.getLanguageId());
155+
const lineTokens = tokenizedLines[tokenizedLines.length - 1] || [];
156+
const sentinelOffset = position.column - 1;
157+
158+
for (let index = lineTokens.length - 1; index >= 0; index--) {
159+
const token = lineTokens[index];
160+
if (token.offset <= sentinelOffset) {
161+
return token.type.startsWith(TokenClassConsts.COMMENT);
162+
}
163+
}
164+
165+
return false;
166+
}
167+
148168
export class CompletionAdapter<T extends BaseSQLWorker>
149169
implements languages.CompletionItemProvider
150170
{
@@ -165,6 +185,10 @@ export class CompletionAdapter<T extends BaseSQLWorker>
165185
context: languages.CompletionContext,
166186
_token: CancellationToken
167187
): Promise<languages.CompletionList> {
188+
if (isPositionInComment(model, position)) {
189+
return Promise.resolve({ suggestions: [] });
190+
}
191+
168192
const resource = model.uri;
169193
return this._worker(resource)
170194
.then((worker) => {
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import * as assert from 'assert';
2+
3+
import { BaseSQLWorker } from '../../baseSQLWorker';
4+
import {
5+
CancellationTokenSource,
6+
editor,
7+
languages,
8+
Position
9+
} from '../../fillers/monaco-editor-core';
10+
import { CompletionAdapter, WorkerAccessor } from '../../languageFeatures';
11+
import { LanguageServiceDefaultsImpl, modeConfigurationDefault } from '../../monaco.contribution';
12+
import { language } from './mysql';
13+
14+
const LANGUAGE_ID = 'mysql-completion-test';
15+
16+
languages.register({ id: LANGUAGE_ID });
17+
languages.setMonarchTokensProvider(LANGUAGE_ID, language);
18+
19+
interface CompletionResult {
20+
suggestions: languages.CompletionItem[];
21+
workerCallCount: number;
22+
}
23+
24+
function getEndPosition(value: string): Position {
25+
const lines = value.split('\n');
26+
return new Position(lines.length, lines[lines.length - 1].length + 1);
27+
}
28+
29+
async function provideCompletionItems(
30+
value: string,
31+
position: Position = getEndPosition(value)
32+
): Promise<CompletionResult> {
33+
const model = editor.createModel(value, LANGUAGE_ID);
34+
const cancellationTokenSource = new CancellationTokenSource();
35+
let workerCallCount = 0;
36+
const worker: WorkerAccessor<BaseSQLWorker> = async () => {
37+
workerCallCount++;
38+
return {
39+
doCompletionWithEntities: async () => ({
40+
suggestions: {
41+
syntax: [],
42+
keywords: ['SELECT']
43+
},
44+
allEntities: null,
45+
context: null
46+
})
47+
} as unknown as BaseSQLWorker;
48+
};
49+
const defaults = new LanguageServiceDefaultsImpl(LANGUAGE_ID, modeConfigurationDefault);
50+
const adapter = new CompletionAdapter(worker, defaults);
51+
52+
try {
53+
const completionList = await adapter.provideCompletionItems(
54+
model,
55+
position,
56+
{ triggerKind: languages.CompletionTriggerKind.Invoke },
57+
cancellationTokenSource.token
58+
);
59+
60+
return {
61+
suggestions: completionList.suggestions,
62+
workerCallCount
63+
};
64+
} finally {
65+
cancellationTokenSource.dispose();
66+
model.dispose();
67+
}
68+
}
69+
70+
test('does not provide completion items after a line comment marker', async () => {
71+
const result = await provideCompletionItems('--');
72+
73+
assert.deepStrictEqual(result.suggestions, []);
74+
assert.strictEqual(result.workerCallCount, 0);
75+
});
76+
77+
test('does not provide completion items inside an inline line comment', async () => {
78+
const result = await provideCompletionItems('SELECT 1 -- comment');
79+
80+
assert.deepStrictEqual(result.suggestions, []);
81+
assert.strictEqual(result.workerCallCount, 0);
82+
});
83+
84+
test('does not provide completion items inside a MySQL hash comment', async () => {
85+
const result = await provideCompletionItems('# comment');
86+
87+
assert.deepStrictEqual(result.suggestions, []);
88+
assert.strictEqual(result.workerCallCount, 0);
89+
});
90+
91+
test('does not provide completion items inside a block comment', async () => {
92+
const result = await provideCompletionItems('/* comment */', new Position(1, 4));
93+
94+
assert.deepStrictEqual(result.suggestions, []);
95+
assert.strictEqual(result.workerCallCount, 0);
96+
});
97+
98+
test('does not provide completion items inside a multiline block comment', async () => {
99+
const result = await provideCompletionItems(
100+
'SELECT /* comment\nstill comment */',
101+
new Position(2, 6)
102+
);
103+
104+
assert.deepStrictEqual(result.suggestions, []);
105+
assert.strictEqual(result.workerCallCount, 0);
106+
});
107+
108+
test('provides completion items after a closed block comment', async () => {
109+
const result = await provideCompletionItems('SELECT /* comment */');
110+
111+
assert.deepStrictEqual(
112+
result.suggestions.map((item) => item.label),
113+
['SELECT']
114+
);
115+
assert.strictEqual(result.workerCallCount, 1);
116+
});
117+
118+
test('does not treat comment markers inside strings as comments', async () => {
119+
const result = await provideCompletionItems("SELECT '--'");
120+
121+
assert.deepStrictEqual(
122+
result.suggestions.map((item) => item.label),
123+
['SELECT']
124+
);
125+
assert.strictEqual(result.workerCallCount, 1);
126+
});
127+
128+
test('does not treat a single minus sign as a comment', async () => {
129+
const result = await provideCompletionItems('-');
130+
131+
assert.deepStrictEqual(
132+
result.suggestions.map((item) => item.label),
133+
['SELECT']
134+
);
135+
assert.strictEqual(result.workerCallCount, 1);
136+
});
137+
138+
test('keeps the existing completion flow for regular SQL', async () => {
139+
const result = await provideCompletionItems('SELECT ');
140+
141+
assert.deepStrictEqual(
142+
result.suggestions.map((item) => item.label),
143+
['SELECT']
144+
);
145+
assert.strictEqual(result.workerCallCount, 1);
146+
});

test/setup.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ define('vs/nls', [], {
2929
}
3030
});
3131

32+
define('dt-sql-parser', [], {
33+
EntityContextType: {
34+
TABLE: 'table',
35+
TABLE_CREATE: 'tableCreate'
36+
}
37+
});
38+
39+
define('monaco-editor', ['vs/editor/editor.main'], function (api) {
40+
return api.m || api;
41+
});
42+
3243
define(['vs/editor/editor.main'], function (api) {
3344
// Monaco Editor 0.54.0+ exports as api.m instead of api directly
3445
const monaco = api.m || api;

0 commit comments

Comments
 (0)