Skip to content

Commit 8f8d5e0

Browse files
committed
fix(css): suppress completions inside CSS/SCSS/LESS comments
When the cursor is inside a CSS, SCSS or LESS comment, the language server still answered completion requests, surfacing suggestions like `:after` after typing `:` in a comment. This bypassed `editor.quickSuggestions.comments: off`, since `:` is registered as a trigger character. Add an `isInsideComment` helper that performs a single forward scan tracking string and comment state, so comment delimiters inside string literals are not mistaken for real comments. Block comments are honoured for all three languages and `//` line comments are honoured for SCSS and LESS. The CSS server returns `null` from `onCompletion` when the position falls inside a comment. Fixes #236215
1 parent 8c15ca4 commit 8f8d5e0

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

extensions/css-language-features/server/src/cssServer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getLanguageModelCache } from './languageModelCache';
1212
import { runSafeAsync } from './utils/runner';
1313
import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation';
1414
import { getDocumentContext } from './utils/documentContext';
15+
import { isInsideComment } from './utils/comments';
1516
import { fetchDataProviders } from './customData';
1617
import { RequestService, getRequestService } from './requests';
1718

@@ -199,6 +200,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
199200
return runSafeAsync(runtime, async () => {
200201
const document = documents.get(textDocumentPosition.textDocument.uri);
201202
if (document) {
203+
const supportsLineComments = document.languageId === 'scss' || document.languageId === 'less';
204+
if (isInsideComment(document.getText(), document.offsetAt(textDocumentPosition.position), supportsLineComments)) {
205+
return null;
206+
}
202207
const [settings,] = await Promise.all([getDocumentSettings(document), dataProvidersReady]);
203208
const styleSheet = stylesheets.get(document);
204209
const documentContext = getDocumentContext(document.uri, workspaceFolders);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import 'mocha';
6+
import * as assert from 'assert';
7+
import { isInsideComment } from '../utils/comments';
8+
9+
suite('isInsideComment', () => {
10+
11+
function at(text: string): { text: string; offset: number } {
12+
const offset = text.indexOf('|');
13+
assert.notStrictEqual(offset, -1, 'test text must contain a | marker');
14+
return { text: text.slice(0, offset) + text.slice(offset + 1), offset };
15+
}
16+
17+
test('plain CSS rule is not in a comment', () => {
18+
const { text, offset } = at('a { col|or: red; }');
19+
assert.strictEqual(isInsideComment(text, offset, false), false);
20+
});
21+
22+
test('inside a closed block comment', () => {
23+
const { text, offset } = at('/* hello | world */');
24+
assert.strictEqual(isInsideComment(text, offset, false), true);
25+
});
26+
27+
test('after a closed block comment', () => {
28+
const { text, offset } = at('/* hello world */ a { co|lor: red }');
29+
assert.strictEqual(isInsideComment(text, offset, false), false);
30+
});
31+
32+
test('inside an unterminated block comment', () => {
33+
const { text, offset } = at('a { color: red } /* trailing | text');
34+
assert.strictEqual(isInsideComment(text, offset, false), true);
35+
});
36+
37+
test('at a colon inside a multi-line block comment (issue #236215)', () => {
38+
const { text, offset } = at('/* element:|\n continued */');
39+
assert.strictEqual(isInsideComment(text, offset, false), true);
40+
});
41+
42+
test('block comment delimiters inside a string literal are ignored', () => {
43+
const { text, offset } = at('a::before { content: \'/*\'; co|lor: red }');
44+
assert.strictEqual(isInsideComment(text, offset, false), false);
45+
});
46+
47+
test('SCSS line comment is detected when supported', () => {
48+
const { text, offset } = at('a { color: red } // line co|mment\nb { }');
49+
assert.strictEqual(isInsideComment(text, offset, true), true);
50+
});
51+
52+
test('SCSS line comment is not detected for plain CSS', () => {
53+
const { text, offset } = at('a { color: red } // line co|mment\nb { }');
54+
assert.strictEqual(isInsideComment(text, offset, false), false);
55+
});
56+
57+
test('after a SCSS line comment is not in a comment', () => {
58+
const { text, offset } = at('// hello\nb { co|lor: red }');
59+
assert.strictEqual(isInsideComment(text, offset, true), false);
60+
});
61+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
const enum CharCode {
7+
Slash = 0x2F,
8+
Asterisk = 0x2A,
9+
Backslash = 0x5C,
10+
DoubleQuote = 0x22,
11+
SingleQuote = 0x27,
12+
}
13+
14+
/**
15+
* Returns whether the given offset falls within a CSS, SCSS or LESS comment.
16+
*
17+
* Performs a single forward scan from the start of the text, tracking string
18+
* and comment state so that occurrences of comment delimiters inside string
19+
* literals are not mistaken for real comments.
20+
*
21+
* Block comments are recognised for all three languages. Line comments are
22+
* only recognised when `supportsLineComments` is `true` (SCSS and LESS).
23+
*/
24+
export function isInsideComment(text: string, offset: number, supportsLineComments: boolean): boolean {
25+
let i = 0;
26+
while (i < offset) {
27+
const ch = text.charCodeAt(i);
28+
// Block comment start
29+
if (ch === CharCode.Slash && text.charCodeAt(i + 1) === CharCode.Asterisk) {
30+
const end = text.indexOf('*/', i + 2);
31+
if (end === -1 || end >= offset) {
32+
return true;
33+
}
34+
i = end + 2;
35+
continue;
36+
}
37+
// Line comment start (SCSS/LESS only)
38+
if (supportsLineComments && ch === CharCode.Slash && text.charCodeAt(i + 1) === CharCode.Slash) {
39+
const nl = text.indexOf('\n', i + 2);
40+
if (nl === -1 || nl >= offset) {
41+
return true;
42+
}
43+
i = nl + 1;
44+
continue;
45+
}
46+
// String literal: skip to matching quote, honouring backslash escapes
47+
if (ch === CharCode.DoubleQuote || ch === CharCode.SingleQuote) {
48+
const quote = ch;
49+
i++;
50+
while (i < offset) {
51+
const c = text.charCodeAt(i);
52+
if (c === CharCode.Backslash) {
53+
i += 2;
54+
continue;
55+
}
56+
if (c === quote) {
57+
i++;
58+
break;
59+
}
60+
i++;
61+
}
62+
continue;
63+
}
64+
i++;
65+
}
66+
return false;
67+
}

0 commit comments

Comments
 (0)