Path: blob/main/src/vs/editor/contrib/comment/browser/lineCommentCommand.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CharCode } from '../../../../base/common/charCode.js';6import * as strings from '../../../../base/common/strings.js';7import { Constants } from '../../../../base/common/uint.js';8import { EditOperation, ISingleEditOperation } from '../../../common/core/editOperation.js';9import { Position } from '../../../common/core/position.js';10import { Range } from '../../../common/core/range.js';11import { Selection } from '../../../common/core/selection.js';12import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../../../common/editorCommon.js';13import { ITextModel } from '../../../common/model.js';14import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';15import { BlockCommentCommand } from './blockCommentCommand.js';1617export interface IInsertionPoint {18ignore: boolean;19commentStrOffset: number;20}2122export interface ILinePreflightData {23ignore: boolean;24commentStr: string;25commentStrOffset: number;26commentStrLength: number;27}2829export interface IPreflightDataSupported {30supported: true;31shouldRemoveComments: boolean;32lines: ILinePreflightData[];33}34export interface IPreflightDataUnsupported {35supported: false;36}37export type IPreflightData = IPreflightDataSupported | IPreflightDataUnsupported;3839export interface ISimpleModel {40getLineContent(lineNumber: number): string;41}4243export const enum Type {44Toggle = 0,45ForceAdd = 1,46ForceRemove = 247}4849export class LineCommentCommand implements ICommand {5051private readonly _selection: Selection;52private readonly _indentSize: number;53private readonly _type: Type;54private readonly _insertSpace: boolean;55private readonly _ignoreEmptyLines: boolean;56private _selectionId: string | null;57private _deltaColumn: number;58private _moveEndPositionDown: boolean;59private _ignoreFirstLine: boolean;6061constructor(62private readonly languageConfigurationService: ILanguageConfigurationService,63selection: Selection,64indentSize: number,65type: Type,66insertSpace: boolean,67ignoreEmptyLines: boolean,68ignoreFirstLine?: boolean,69) {70this._selection = selection;71this._indentSize = indentSize;72this._type = type;73this._insertSpace = insertSpace;74this._selectionId = null;75this._deltaColumn = 0;76this._moveEndPositionDown = false;77this._ignoreEmptyLines = ignoreEmptyLines;78this._ignoreFirstLine = ignoreFirstLine || false;79}8081/**82* Do an initial pass over the lines and gather info about the line comment string.83* Returns null if any of the lines doesn't support a line comment string.84*/85private static _gatherPreflightCommentStrings(model: ITextModel, startLineNumber: number, endLineNumber: number, languageConfigurationService: ILanguageConfigurationService): ILinePreflightData[] | null {8687model.tokenization.tokenizeIfCheap(startLineNumber);88const languageId = model.getLanguageIdAtPosition(startLineNumber, 1);8990const config = languageConfigurationService.getLanguageConfiguration(languageId).comments;91const commentStr = (config ? config.lineCommentToken : null);92if (!commentStr) {93// Mode does not support line comments94return null;95}9697const lines: ILinePreflightData[] = [];98for (let i = 0, lineCount = endLineNumber - startLineNumber + 1; i < lineCount; i++) {99lines[i] = {100ignore: false,101commentStr: commentStr,102commentStrOffset: 0,103commentStrLength: commentStr.length104};105}106107return lines;108}109110/**111* Analyze lines and decide which lines are relevant and what the toggle should do.112* Also, build up several offsets and lengths useful in the generation of editor operations.113*/114public static _analyzeLines(type: Type, insertSpace: boolean, model: ISimpleModel, lines: ILinePreflightData[], startLineNumber: number, ignoreEmptyLines: boolean, ignoreFirstLine: boolean, languageConfigurationService: ILanguageConfigurationService, languageId: string): IPreflightData {115let onlyWhitespaceLines = true;116117const config = languageConfigurationService.getLanguageConfiguration(languageId).comments;118const lineCommentNoIndent = config?.lineCommentNoIndent ?? false;119120let shouldRemoveComments: boolean;121if (type === Type.Toggle) {122shouldRemoveComments = true;123} else if (type === Type.ForceAdd) {124shouldRemoveComments = false;125} else {126shouldRemoveComments = true;127}128129for (let i = 0, lineCount = lines.length; i < lineCount; i++) {130const lineData = lines[i];131const lineNumber = startLineNumber + i;132133if (lineNumber === startLineNumber && ignoreFirstLine) {134// first line ignored135lineData.ignore = true;136continue;137}138139const lineContent = model.getLineContent(lineNumber);140const lineContentStartOffset = strings.firstNonWhitespaceIndex(lineContent);141142if (lineContentStartOffset === -1) {143// Empty or whitespace only line144lineData.ignore = ignoreEmptyLines;145lineData.commentStrOffset = lineCommentNoIndent ? 0 : lineContent.length;146continue;147}148149onlyWhitespaceLines = false;150const offset = lineCommentNoIndent ? 0 : lineContentStartOffset;151lineData.ignore = false;152lineData.commentStrOffset = offset;153154if (shouldRemoveComments && !BlockCommentCommand._haystackHasNeedleAtOffset(lineContent, lineData.commentStr, offset)) {155if (type === Type.Toggle) {156// Every line so far has been a line comment, but this one is not157shouldRemoveComments = false;158} else if (type === Type.ForceAdd) {159// Will not happen160} else {161lineData.ignore = true;162}163}164165if (shouldRemoveComments && insertSpace) {166// Remove a following space if present167const commentStrEndOffset = lineContentStartOffset + lineData.commentStrLength;168if (commentStrEndOffset < lineContent.length && lineContent.charCodeAt(commentStrEndOffset) === CharCode.Space) {169lineData.commentStrLength += 1;170}171}172}173174if (type === Type.Toggle && onlyWhitespaceLines) {175// For only whitespace lines, we insert comments176shouldRemoveComments = false;177178// Also, no longer ignore them179for (let i = 0, lineCount = lines.length; i < lineCount; i++) {180lines[i].ignore = false;181}182}183184return {185supported: true,186shouldRemoveComments: shouldRemoveComments,187lines: lines188};189}190191/**192* Analyze all lines and decide exactly what to do => not supported | insert line comments | remove line comments193*/194public static _gatherPreflightData(type: Type, insertSpace: boolean, model: ITextModel, startLineNumber: number, endLineNumber: number, ignoreEmptyLines: boolean, ignoreFirstLine: boolean, languageConfigurationService: ILanguageConfigurationService): IPreflightData {195const lines = LineCommentCommand._gatherPreflightCommentStrings(model, startLineNumber, endLineNumber, languageConfigurationService);196const languageId = model.getLanguageIdAtPosition(startLineNumber, 1);197if (lines === null) {198return {199supported: false200};201}202203return LineCommentCommand._analyzeLines(type, insertSpace, model, lines, startLineNumber, ignoreEmptyLines, ignoreFirstLine, languageConfigurationService, languageId);204}205206/**207* Given a successful analysis, execute either insert line comments, either remove line comments208*/209private _executeLineComments(model: ISimpleModel, builder: IEditOperationBuilder, data: IPreflightDataSupported, s: Selection): void {210211let ops: ISingleEditOperation[];212213if (data.shouldRemoveComments) {214ops = LineCommentCommand._createRemoveLineCommentsOperations(data.lines, s.startLineNumber);215} else {216LineCommentCommand._normalizeInsertionPoint(model, data.lines, s.startLineNumber, this._indentSize);217ops = this._createAddLineCommentsOperations(data.lines, s.startLineNumber);218}219220const cursorPosition = new Position(s.positionLineNumber, s.positionColumn);221222for (let i = 0, len = ops.length; i < len; i++) {223builder.addEditOperation(ops[i].range, ops[i].text);224if (Range.isEmpty(ops[i].range) && Range.getStartPosition(ops[i].range).equals(cursorPosition)) {225const lineContent = model.getLineContent(cursorPosition.lineNumber);226if (lineContent.length + 1 === cursorPosition.column) {227this._deltaColumn = (ops[i].text || '').length;228}229}230}231232this._selectionId = builder.trackSelection(s);233}234235private _attemptRemoveBlockComment(model: ITextModel, s: Selection, startToken: string, endToken: string): ISingleEditOperation[] | null {236let startLineNumber = s.startLineNumber;237let endLineNumber = s.endLineNumber;238239const startTokenAllowedBeforeColumn = endToken.length + Math.max(240model.getLineFirstNonWhitespaceColumn(s.startLineNumber),241s.startColumn242);243244let startTokenIndex = model.getLineContent(startLineNumber).lastIndexOf(startToken, startTokenAllowedBeforeColumn - 1);245let endTokenIndex = model.getLineContent(endLineNumber).indexOf(endToken, s.endColumn - 1 - startToken.length);246247if (startTokenIndex !== -1 && endTokenIndex === -1) {248endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);249endLineNumber = startLineNumber;250}251252if (startTokenIndex === -1 && endTokenIndex !== -1) {253startTokenIndex = model.getLineContent(endLineNumber).lastIndexOf(startToken, endTokenIndex);254startLineNumber = endLineNumber;255}256257if (s.isEmpty() && (startTokenIndex === -1 || endTokenIndex === -1)) {258startTokenIndex = model.getLineContent(startLineNumber).indexOf(startToken);259if (startTokenIndex !== -1) {260endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);261}262}263264// We have to adjust to possible inner white space.265// For Space after startToken, add Space to startToken - range math will work out.266if (startTokenIndex !== -1 && model.getLineContent(startLineNumber).charCodeAt(startTokenIndex + startToken.length) === CharCode.Space) {267startToken += ' ';268}269270// For Space before endToken, add Space before endToken and shift index one left.271if (endTokenIndex !== -1 && model.getLineContent(endLineNumber).charCodeAt(endTokenIndex - 1) === CharCode.Space) {272endToken = ' ' + endToken;273endTokenIndex -= 1;274}275276if (startTokenIndex !== -1 && endTokenIndex !== -1) {277return BlockCommentCommand._createRemoveBlockCommentOperations(278new Range(startLineNumber, startTokenIndex + startToken.length + 1, endLineNumber, endTokenIndex + 1), startToken, endToken279);280}281282return null;283}284285/**286* Given an unsuccessful analysis, delegate to the block comment command287*/288private _executeBlockComment(model: ITextModel, builder: IEditOperationBuilder, s: Selection): void {289model.tokenization.tokenizeIfCheap(s.startLineNumber);290const languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1);291const config = this.languageConfigurationService.getLanguageConfiguration(languageId).comments;292if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) {293// Mode does not support block comments294return;295}296297const startToken = config.blockCommentStartToken;298const endToken = config.blockCommentEndToken;299300let ops = this._attemptRemoveBlockComment(model, s, startToken, endToken);301if (!ops) {302if (s.isEmpty()) {303const lineContent = model.getLineContent(s.startLineNumber);304let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);305if (firstNonWhitespaceIndex === -1) {306// Line is empty or contains only whitespace307firstNonWhitespaceIndex = lineContent.length;308}309ops = BlockCommentCommand._createAddBlockCommentOperations(310new Range(s.startLineNumber, firstNonWhitespaceIndex + 1, s.startLineNumber, lineContent.length + 1),311startToken,312endToken,313this._insertSpace314);315} else {316ops = BlockCommentCommand._createAddBlockCommentOperations(317new Range(s.startLineNumber, model.getLineFirstNonWhitespaceColumn(s.startLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)),318startToken,319endToken,320this._insertSpace321);322}323324if (ops.length === 1) {325// Leave cursor after token and Space326this._deltaColumn = startToken.length + 1;327}328}329this._selectionId = builder.trackSelection(s);330for (const op of ops) {331builder.addEditOperation(op.range, op.text);332}333}334335public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {336337let s = this._selection;338this._moveEndPositionDown = false;339340if (s.startLineNumber === s.endLineNumber && this._ignoreFirstLine) {341builder.addEditOperation(new Range(s.startLineNumber, model.getLineMaxColumn(s.startLineNumber), s.startLineNumber + 1, 1), s.startLineNumber === model.getLineCount() ? '' : '\n');342this._selectionId = builder.trackSelection(s);343return;344}345346if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {347this._moveEndPositionDown = true;348s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));349}350351const data = LineCommentCommand._gatherPreflightData(352this._type,353this._insertSpace,354model,355s.startLineNumber,356s.endLineNumber,357this._ignoreEmptyLines,358this._ignoreFirstLine,359this.languageConfigurationService360);361362if (data.supported) {363return this._executeLineComments(model, builder, data, s);364}365366return this._executeBlockComment(model, builder, s);367}368369public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {370let result = helper.getTrackedSelection(this._selectionId!);371372if (this._moveEndPositionDown) {373result = result.setEndPosition(result.endLineNumber + 1, 1);374}375376return new Selection(377result.selectionStartLineNumber,378result.selectionStartColumn + this._deltaColumn,379result.positionLineNumber,380result.positionColumn + this._deltaColumn381);382}383384/**385* Generate edit operations in the remove line comment case386*/387public static _createRemoveLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): ISingleEditOperation[] {388const res: ISingleEditOperation[] = [];389390for (let i = 0, len = lines.length; i < len; i++) {391const lineData = lines[i];392393if (lineData.ignore) {394continue;395}396397res.push(EditOperation.delete(new Range(398startLineNumber + i, lineData.commentStrOffset + 1,399startLineNumber + i, lineData.commentStrOffset + lineData.commentStrLength + 1400)));401}402403return res;404}405406/**407* Generate edit operations in the add line comment case408*/409private _createAddLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): ISingleEditOperation[] {410const res: ISingleEditOperation[] = [];411const afterCommentStr = this._insertSpace ? ' ' : '';412413414for (let i = 0, len = lines.length; i < len; i++) {415const lineData = lines[i];416417if (lineData.ignore) {418continue;419}420421res.push(EditOperation.insert(new Position(startLineNumber + i, lineData.commentStrOffset + 1), lineData.commentStr + afterCommentStr));422}423424return res;425}426427private static nextVisibleColumn(currentVisibleColumn: number, indentSize: number, isTab: boolean, columnSize: number): number {428if (isTab) {429return currentVisibleColumn + (indentSize - (currentVisibleColumn % indentSize));430}431return currentVisibleColumn + columnSize;432}433434/**435* Adjust insertion points to have them vertically aligned in the add line comment case436*/437public static _normalizeInsertionPoint(model: ISimpleModel, lines: IInsertionPoint[], startLineNumber: number, indentSize: number): void {438let minVisibleColumn = Constants.MAX_SAFE_SMALL_INTEGER;439let j: number;440let lenJ: number;441442for (let i = 0, len = lines.length; i < len; i++) {443if (lines[i].ignore) {444continue;445}446447const lineContent = model.getLineContent(startLineNumber + i);448449let currentVisibleColumn = 0;450for (let j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {451currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, indentSize, lineContent.charCodeAt(j) === CharCode.Tab, 1);452}453454if (currentVisibleColumn < minVisibleColumn) {455minVisibleColumn = currentVisibleColumn;456}457}458459minVisibleColumn = Math.floor(minVisibleColumn / indentSize) * indentSize;460461for (let i = 0, len = lines.length; i < len; i++) {462if (lines[i].ignore) {463continue;464}465466const lineContent = model.getLineContent(startLineNumber + i);467468let currentVisibleColumn = 0;469for (j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {470currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, indentSize, lineContent.charCodeAt(j) === CharCode.Tab, 1);471}472473if (currentVisibleColumn > minVisibleColumn) {474lines[i].commentStrOffset = j - 1;475} else {476lines[i].commentStrOffset = j;477}478}479}480}481482483