Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts
3296 views
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
import { CharCode } from '../../../../base/common/charCode.js';
7
import * as strings from '../../../../base/common/strings.js';
8
import { Constants } from '../../../../base/common/uint.js';
9
import { EditOperation, ISingleEditOperation } from '../../../common/core/editOperation.js';
10
import { Position } from '../../../common/core/position.js';
11
import { Range } from '../../../common/core/range.js';
12
import { Selection } from '../../../common/core/selection.js';
13
import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../../../common/editorCommon.js';
14
import { ITextModel } from '../../../common/model.js';
15
import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';
16
import { BlockCommentCommand } from './blockCommentCommand.js';
17
18
export interface IInsertionPoint {
19
ignore: boolean;
20
commentStrOffset: number;
21
}
22
23
export interface ILinePreflightData {
24
ignore: boolean;
25
commentStr: string;
26
commentStrOffset: number;
27
commentStrLength: number;
28
}
29
30
export interface IPreflightDataSupported {
31
supported: true;
32
shouldRemoveComments: boolean;
33
lines: ILinePreflightData[];
34
}
35
export interface IPreflightDataUnsupported {
36
supported: false;
37
}
38
export type IPreflightData = IPreflightDataSupported | IPreflightDataUnsupported;
39
40
export interface ISimpleModel {
41
getLineContent(lineNumber: number): string;
42
}
43
44
export const enum Type {
45
Toggle = 0,
46
ForceAdd = 1,
47
ForceRemove = 2
48
}
49
50
export class LineCommentCommand implements ICommand {
51
52
private readonly _selection: Selection;
53
private readonly _indentSize: number;
54
private readonly _type: Type;
55
private readonly _insertSpace: boolean;
56
private readonly _ignoreEmptyLines: boolean;
57
private _selectionId: string | null;
58
private _deltaColumn: number;
59
private _moveEndPositionDown: boolean;
60
private _ignoreFirstLine: boolean;
61
62
constructor(
63
private readonly languageConfigurationService: ILanguageConfigurationService,
64
selection: Selection,
65
indentSize: number,
66
type: Type,
67
insertSpace: boolean,
68
ignoreEmptyLines: boolean,
69
ignoreFirstLine?: boolean,
70
) {
71
this._selection = selection;
72
this._indentSize = indentSize;
73
this._type = type;
74
this._insertSpace = insertSpace;
75
this._selectionId = null;
76
this._deltaColumn = 0;
77
this._moveEndPositionDown = false;
78
this._ignoreEmptyLines = ignoreEmptyLines;
79
this._ignoreFirstLine = ignoreFirstLine || false;
80
}
81
82
/**
83
* Do an initial pass over the lines and gather info about the line comment string.
84
* Returns null if any of the lines doesn't support a line comment string.
85
*/
86
private static _gatherPreflightCommentStrings(model: ITextModel, startLineNumber: number, endLineNumber: number, languageConfigurationService: ILanguageConfigurationService): ILinePreflightData[] | null {
87
88
model.tokenization.tokenizeIfCheap(startLineNumber);
89
const languageId = model.getLanguageIdAtPosition(startLineNumber, 1);
90
91
const config = languageConfigurationService.getLanguageConfiguration(languageId).comments;
92
const commentStr = (config ? config.lineCommentToken : null);
93
if (!commentStr) {
94
// Mode does not support line comments
95
return null;
96
}
97
98
const lines: ILinePreflightData[] = [];
99
for (let i = 0, lineCount = endLineNumber - startLineNumber + 1; i < lineCount; i++) {
100
lines[i] = {
101
ignore: false,
102
commentStr: commentStr,
103
commentStrOffset: 0,
104
commentStrLength: commentStr.length
105
};
106
}
107
108
return lines;
109
}
110
111
/**
112
* Analyze lines and decide which lines are relevant and what the toggle should do.
113
* Also, build up several offsets and lengths useful in the generation of editor operations.
114
*/
115
public static _analyzeLines(type: Type, insertSpace: boolean, model: ISimpleModel, lines: ILinePreflightData[], startLineNumber: number, ignoreEmptyLines: boolean, ignoreFirstLine: boolean, languageConfigurationService: ILanguageConfigurationService, languageId: string): IPreflightData {
116
let onlyWhitespaceLines = true;
117
118
const config = languageConfigurationService.getLanguageConfiguration(languageId).comments;
119
const lineCommentNoIndent = config?.lineCommentNoIndent ?? false;
120
121
let shouldRemoveComments: boolean;
122
if (type === Type.Toggle) {
123
shouldRemoveComments = true;
124
} else if (type === Type.ForceAdd) {
125
shouldRemoveComments = false;
126
} else {
127
shouldRemoveComments = true;
128
}
129
130
for (let i = 0, lineCount = lines.length; i < lineCount; i++) {
131
const lineData = lines[i];
132
const lineNumber = startLineNumber + i;
133
134
if (lineNumber === startLineNumber && ignoreFirstLine) {
135
// first line ignored
136
lineData.ignore = true;
137
continue;
138
}
139
140
const lineContent = model.getLineContent(lineNumber);
141
const lineContentStartOffset = strings.firstNonWhitespaceIndex(lineContent);
142
143
if (lineContentStartOffset === -1) {
144
// Empty or whitespace only line
145
lineData.ignore = ignoreEmptyLines;
146
lineData.commentStrOffset = lineCommentNoIndent ? 0 : lineContent.length;
147
continue;
148
}
149
150
onlyWhitespaceLines = false;
151
const offset = lineCommentNoIndent ? 0 : lineContentStartOffset;
152
lineData.ignore = false;
153
lineData.commentStrOffset = offset;
154
155
if (shouldRemoveComments && !BlockCommentCommand._haystackHasNeedleAtOffset(lineContent, lineData.commentStr, offset)) {
156
if (type === Type.Toggle) {
157
// Every line so far has been a line comment, but this one is not
158
shouldRemoveComments = false;
159
} else if (type === Type.ForceAdd) {
160
// Will not happen
161
} else {
162
lineData.ignore = true;
163
}
164
}
165
166
if (shouldRemoveComments && insertSpace) {
167
// Remove a following space if present
168
const commentStrEndOffset = lineContentStartOffset + lineData.commentStrLength;
169
if (commentStrEndOffset < lineContent.length && lineContent.charCodeAt(commentStrEndOffset) === CharCode.Space) {
170
lineData.commentStrLength += 1;
171
}
172
}
173
}
174
175
if (type === Type.Toggle && onlyWhitespaceLines) {
176
// For only whitespace lines, we insert comments
177
shouldRemoveComments = false;
178
179
// Also, no longer ignore them
180
for (let i = 0, lineCount = lines.length; i < lineCount; i++) {
181
lines[i].ignore = false;
182
}
183
}
184
185
return {
186
supported: true,
187
shouldRemoveComments: shouldRemoveComments,
188
lines: lines
189
};
190
}
191
192
/**
193
* Analyze all lines and decide exactly what to do => not supported | insert line comments | remove line comments
194
*/
195
public static _gatherPreflightData(type: Type, insertSpace: boolean, model: ITextModel, startLineNumber: number, endLineNumber: number, ignoreEmptyLines: boolean, ignoreFirstLine: boolean, languageConfigurationService: ILanguageConfigurationService): IPreflightData {
196
const lines = LineCommentCommand._gatherPreflightCommentStrings(model, startLineNumber, endLineNumber, languageConfigurationService);
197
const languageId = model.getLanguageIdAtPosition(startLineNumber, 1);
198
if (lines === null) {
199
return {
200
supported: false
201
};
202
}
203
204
return LineCommentCommand._analyzeLines(type, insertSpace, model, lines, startLineNumber, ignoreEmptyLines, ignoreFirstLine, languageConfigurationService, languageId);
205
}
206
207
/**
208
* Given a successful analysis, execute either insert line comments, either remove line comments
209
*/
210
private _executeLineComments(model: ISimpleModel, builder: IEditOperationBuilder, data: IPreflightDataSupported, s: Selection): void {
211
212
let ops: ISingleEditOperation[];
213
214
if (data.shouldRemoveComments) {
215
ops = LineCommentCommand._createRemoveLineCommentsOperations(data.lines, s.startLineNumber);
216
} else {
217
LineCommentCommand._normalizeInsertionPoint(model, data.lines, s.startLineNumber, this._indentSize);
218
ops = this._createAddLineCommentsOperations(data.lines, s.startLineNumber);
219
}
220
221
const cursorPosition = new Position(s.positionLineNumber, s.positionColumn);
222
223
for (let i = 0, len = ops.length; i < len; i++) {
224
builder.addEditOperation(ops[i].range, ops[i].text);
225
if (Range.isEmpty(ops[i].range) && Range.getStartPosition(ops[i].range).equals(cursorPosition)) {
226
const lineContent = model.getLineContent(cursorPosition.lineNumber);
227
if (lineContent.length + 1 === cursorPosition.column) {
228
this._deltaColumn = (ops[i].text || '').length;
229
}
230
}
231
}
232
233
this._selectionId = builder.trackSelection(s);
234
}
235
236
private _attemptRemoveBlockComment(model: ITextModel, s: Selection, startToken: string, endToken: string): ISingleEditOperation[] | null {
237
let startLineNumber = s.startLineNumber;
238
let endLineNumber = s.endLineNumber;
239
240
const startTokenAllowedBeforeColumn = endToken.length + Math.max(
241
model.getLineFirstNonWhitespaceColumn(s.startLineNumber),
242
s.startColumn
243
);
244
245
let startTokenIndex = model.getLineContent(startLineNumber).lastIndexOf(startToken, startTokenAllowedBeforeColumn - 1);
246
let endTokenIndex = model.getLineContent(endLineNumber).indexOf(endToken, s.endColumn - 1 - startToken.length);
247
248
if (startTokenIndex !== -1 && endTokenIndex === -1) {
249
endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);
250
endLineNumber = startLineNumber;
251
}
252
253
if (startTokenIndex === -1 && endTokenIndex !== -1) {
254
startTokenIndex = model.getLineContent(endLineNumber).lastIndexOf(startToken, endTokenIndex);
255
startLineNumber = endLineNumber;
256
}
257
258
if (s.isEmpty() && (startTokenIndex === -1 || endTokenIndex === -1)) {
259
startTokenIndex = model.getLineContent(startLineNumber).indexOf(startToken);
260
if (startTokenIndex !== -1) {
261
endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);
262
}
263
}
264
265
// We have to adjust to possible inner white space.
266
// For Space after startToken, add Space to startToken - range math will work out.
267
if (startTokenIndex !== -1 && model.getLineContent(startLineNumber).charCodeAt(startTokenIndex + startToken.length) === CharCode.Space) {
268
startToken += ' ';
269
}
270
271
// For Space before endToken, add Space before endToken and shift index one left.
272
if (endTokenIndex !== -1 && model.getLineContent(endLineNumber).charCodeAt(endTokenIndex - 1) === CharCode.Space) {
273
endToken = ' ' + endToken;
274
endTokenIndex -= 1;
275
}
276
277
if (startTokenIndex !== -1 && endTokenIndex !== -1) {
278
return BlockCommentCommand._createRemoveBlockCommentOperations(
279
new Range(startLineNumber, startTokenIndex + startToken.length + 1, endLineNumber, endTokenIndex + 1), startToken, endToken
280
);
281
}
282
283
return null;
284
}
285
286
/**
287
* Given an unsuccessful analysis, delegate to the block comment command
288
*/
289
private _executeBlockComment(model: ITextModel, builder: IEditOperationBuilder, s: Selection): void {
290
model.tokenization.tokenizeIfCheap(s.startLineNumber);
291
const languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1);
292
const config = this.languageConfigurationService.getLanguageConfiguration(languageId).comments;
293
if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) {
294
// Mode does not support block comments
295
return;
296
}
297
298
const startToken = config.blockCommentStartToken;
299
const endToken = config.blockCommentEndToken;
300
301
let ops = this._attemptRemoveBlockComment(model, s, startToken, endToken);
302
if (!ops) {
303
if (s.isEmpty()) {
304
const lineContent = model.getLineContent(s.startLineNumber);
305
let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
306
if (firstNonWhitespaceIndex === -1) {
307
// Line is empty or contains only whitespace
308
firstNonWhitespaceIndex = lineContent.length;
309
}
310
ops = BlockCommentCommand._createAddBlockCommentOperations(
311
new Range(s.startLineNumber, firstNonWhitespaceIndex + 1, s.startLineNumber, lineContent.length + 1),
312
startToken,
313
endToken,
314
this._insertSpace
315
);
316
} else {
317
ops = BlockCommentCommand._createAddBlockCommentOperations(
318
new Range(s.startLineNumber, model.getLineFirstNonWhitespaceColumn(s.startLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)),
319
startToken,
320
endToken,
321
this._insertSpace
322
);
323
}
324
325
if (ops.length === 1) {
326
// Leave cursor after token and Space
327
this._deltaColumn = startToken.length + 1;
328
}
329
}
330
this._selectionId = builder.trackSelection(s);
331
for (const op of ops) {
332
builder.addEditOperation(op.range, op.text);
333
}
334
}
335
336
public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
337
338
let s = this._selection;
339
this._moveEndPositionDown = false;
340
341
if (s.startLineNumber === s.endLineNumber && this._ignoreFirstLine) {
342
builder.addEditOperation(new Range(s.startLineNumber, model.getLineMaxColumn(s.startLineNumber), s.startLineNumber + 1, 1), s.startLineNumber === model.getLineCount() ? '' : '\n');
343
this._selectionId = builder.trackSelection(s);
344
return;
345
}
346
347
if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
348
this._moveEndPositionDown = true;
349
s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));
350
}
351
352
const data = LineCommentCommand._gatherPreflightData(
353
this._type,
354
this._insertSpace,
355
model,
356
s.startLineNumber,
357
s.endLineNumber,
358
this._ignoreEmptyLines,
359
this._ignoreFirstLine,
360
this.languageConfigurationService
361
);
362
363
if (data.supported) {
364
return this._executeLineComments(model, builder, data, s);
365
}
366
367
return this._executeBlockComment(model, builder, s);
368
}
369
370
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
371
let result = helper.getTrackedSelection(this._selectionId!);
372
373
if (this._moveEndPositionDown) {
374
result = result.setEndPosition(result.endLineNumber + 1, 1);
375
}
376
377
return new Selection(
378
result.selectionStartLineNumber,
379
result.selectionStartColumn + this._deltaColumn,
380
result.positionLineNumber,
381
result.positionColumn + this._deltaColumn
382
);
383
}
384
385
/**
386
* Generate edit operations in the remove line comment case
387
*/
388
public static _createRemoveLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): ISingleEditOperation[] {
389
const res: ISingleEditOperation[] = [];
390
391
for (let i = 0, len = lines.length; i < len; i++) {
392
const lineData = lines[i];
393
394
if (lineData.ignore) {
395
continue;
396
}
397
398
res.push(EditOperation.delete(new Range(
399
startLineNumber + i, lineData.commentStrOffset + 1,
400
startLineNumber + i, lineData.commentStrOffset + lineData.commentStrLength + 1
401
)));
402
}
403
404
return res;
405
}
406
407
/**
408
* Generate edit operations in the add line comment case
409
*/
410
private _createAddLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): ISingleEditOperation[] {
411
const res: ISingleEditOperation[] = [];
412
const afterCommentStr = this._insertSpace ? ' ' : '';
413
414
415
for (let i = 0, len = lines.length; i < len; i++) {
416
const lineData = lines[i];
417
418
if (lineData.ignore) {
419
continue;
420
}
421
422
res.push(EditOperation.insert(new Position(startLineNumber + i, lineData.commentStrOffset + 1), lineData.commentStr + afterCommentStr));
423
}
424
425
return res;
426
}
427
428
private static nextVisibleColumn(currentVisibleColumn: number, indentSize: number, isTab: boolean, columnSize: number): number {
429
if (isTab) {
430
return currentVisibleColumn + (indentSize - (currentVisibleColumn % indentSize));
431
}
432
return currentVisibleColumn + columnSize;
433
}
434
435
/**
436
* Adjust insertion points to have them vertically aligned in the add line comment case
437
*/
438
public static _normalizeInsertionPoint(model: ISimpleModel, lines: IInsertionPoint[], startLineNumber: number, indentSize: number): void {
439
let minVisibleColumn = Constants.MAX_SAFE_SMALL_INTEGER;
440
let j: number;
441
let lenJ: number;
442
443
for (let i = 0, len = lines.length; i < len; i++) {
444
if (lines[i].ignore) {
445
continue;
446
}
447
448
const lineContent = model.getLineContent(startLineNumber + i);
449
450
let currentVisibleColumn = 0;
451
for (let j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {
452
currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, indentSize, lineContent.charCodeAt(j) === CharCode.Tab, 1);
453
}
454
455
if (currentVisibleColumn < minVisibleColumn) {
456
minVisibleColumn = currentVisibleColumn;
457
}
458
}
459
460
minVisibleColumn = Math.floor(minVisibleColumn / indentSize) * indentSize;
461
462
for (let i = 0, len = lines.length; i < len; i++) {
463
if (lines[i].ignore) {
464
continue;
465
}
466
467
const lineContent = model.getLineContent(startLineNumber + i);
468
469
let currentVisibleColumn = 0;
470
for (j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {
471
currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, indentSize, lineContent.charCodeAt(j) === CharCode.Tab, 1);
472
}
473
474
if (currentVisibleColumn > minVisibleColumn) {
475
lines[i].commentStrOffset = j - 1;
476
} else {
477
lines[i].commentStrOffset = j;
478
}
479
}
480
}
481
}
482
483