Path: blob/main/extensions/merge-conflict/src/mergeConflictParser.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*--------------------------------------------------------------------------------------------*/4import * as vscode from 'vscode';5import * as interfaces from './interfaces';6import { DocumentMergeConflict } from './documentMergeConflict';7import TelemetryReporter from '@vscode/extension-telemetry';89const startHeaderMarker = '<<<<<<<';10const commonAncestorsMarker = '|||||||';11const splitterMarker = '=======';12const endFooterMarker = '>>>>>>>';1314interface IScanMergedConflict {15startHeader: vscode.TextLine;16commonAncestors: vscode.TextLine[];17splitter?: vscode.TextLine;18endFooter?: vscode.TextLine;19}2021export class MergeConflictParser {2223static scanDocument(document: vscode.TextDocument, telemetryReporter: TelemetryReporter): interfaces.IDocumentMergeConflict[] {2425// Scan each line in the document, we already know there is at least a <<<<<<< and26// >>>>>> marker within the document, we need to group these into conflict ranges.27// We initially build a scan match, that references the lines of the header, splitter28// and footer. This is then converted into a full descriptor containing all required29// ranges.3031let currentConflict: IScanMergedConflict | null = null;32const conflictDescriptors: interfaces.IDocumentMergeConflictDescriptor[] = [];3334for (let i = 0; i < document.lineCount; i++) {35const line = document.lineAt(i);3637// Ignore empty lines38if (!line || line.isEmptyOrWhitespace) {39continue;40}4142// Is this a start line? <<<<<<<43if (line.text.startsWith(startHeaderMarker)) {44if (currentConflict !== null) {45// Error, we should not see a startMarker before we've seen an endMarker46currentConflict = null;4748// Give up parsing, anything matched up this to this point will be decorated49// anything after will not50break;51}5253// Create a new conflict starting at this line54currentConflict = { startHeader: line, commonAncestors: [] };55}56// Are we within a conflict block and is this a common ancestors marker? |||||||57else if (currentConflict && !currentConflict.splitter && line.text.startsWith(commonAncestorsMarker)) {58currentConflict.commonAncestors.push(line);59}60// Are we within a conflict block and is this a splitter? =======61else if (currentConflict && !currentConflict.splitter && line.text === splitterMarker) {62currentConflict.splitter = line;63}64// Are we within a conflict block and is this a footer? >>>>>>>65else if (currentConflict && line.text.startsWith(endFooterMarker)) {66currentConflict.endFooter = line;6768// Create a full descriptor from the lines that we matched. This can return69// null if the descriptor could not be completed.70const completeDescriptor = MergeConflictParser.scanItemTolMergeConflictDescriptor(document, currentConflict);7172if (completeDescriptor !== null) {73conflictDescriptors.push(completeDescriptor);74}7576// Reset the current conflict to be empty, so we can match the next77// starting header marker.78currentConflict = null;79}80}8182return conflictDescriptors83.filter(Boolean)84.map(descriptor => new DocumentMergeConflict(descriptor, telemetryReporter));85}8687private static scanItemTolMergeConflictDescriptor(document: vscode.TextDocument, scanned: IScanMergedConflict): interfaces.IDocumentMergeConflictDescriptor | null {88// Validate we have all the required lines within the scan item.89if (!scanned.startHeader || !scanned.splitter || !scanned.endFooter) {90return null;91}9293const tokenAfterCurrentBlock: vscode.TextLine = scanned.commonAncestors[0] || scanned.splitter;9495// Assume that descriptor.current.header, descriptor.incoming.header and descriptor.splitter96// have valid ranges, fill in content and total ranges from these parts.97// NOTE: We need to shift the decorator range back one character so the splitter does not end up with98// two decoration colors (current and splitter), if we take the new line from the content into account99// the decorator will wrap to the next line.100return {101current: {102header: scanned.startHeader.range,103decoratorContent: new vscode.Range(104scanned.startHeader.rangeIncludingLineBreak.end,105MergeConflictParser.shiftBackOneCharacter(document, tokenAfterCurrentBlock.range.start, scanned.startHeader.rangeIncludingLineBreak.end)),106// Current content is range between header (shifted for linebreak) and splitter or common ancestors mark start107content: new vscode.Range(108scanned.startHeader.rangeIncludingLineBreak.end,109tokenAfterCurrentBlock.range.start),110name: scanned.startHeader.text.substring(startHeaderMarker.length + 1)111},112commonAncestors: scanned.commonAncestors.map((currentTokenLine, index, commonAncestors) => {113const nextTokenLine = commonAncestors[index + 1] || scanned.splitter;114return {115header: currentTokenLine.range,116decoratorContent: new vscode.Range(117currentTokenLine.rangeIncludingLineBreak.end,118MergeConflictParser.shiftBackOneCharacter(document, nextTokenLine.range.start, currentTokenLine.rangeIncludingLineBreak.end)),119// Each common ancestors block is range between one common ancestors token120// (shifted for linebreak) and start of next common ancestors token or splitter121content: new vscode.Range(122currentTokenLine.rangeIncludingLineBreak.end,123nextTokenLine.range.start),124name: currentTokenLine.text.substring(commonAncestorsMarker.length + 1)125};126}),127splitter: scanned.splitter.range,128incoming: {129header: scanned.endFooter.range,130decoratorContent: new vscode.Range(131scanned.splitter.rangeIncludingLineBreak.end,132MergeConflictParser.shiftBackOneCharacter(document, scanned.endFooter.range.start, scanned.splitter.rangeIncludingLineBreak.end)),133// Incoming content is range between splitter (shifted for linebreak) and footer start134content: new vscode.Range(135scanned.splitter.rangeIncludingLineBreak.end,136scanned.endFooter.range.start),137name: scanned.endFooter.text.substring(endFooterMarker.length + 1)138},139// Entire range is between current header start and incoming header end (including line break)140range: new vscode.Range(scanned.startHeader.range.start, scanned.endFooter.rangeIncludingLineBreak.end)141};142}143144static containsConflict(document: vscode.TextDocument): boolean {145if (!document) {146return false;147}148149const text = document.getText();150return text.includes(startHeaderMarker) && text.includes(endFooterMarker);151}152153private static shiftBackOneCharacter(document: vscode.TextDocument, range: vscode.Position, unlessEqual: vscode.Position): vscode.Position {154if (range.isEqual(unlessEqual)) {155return range;156}157158let line = range.line;159let character = range.character - 1;160161if (character < 0) {162line--;163character = document.lineAt(line).range.end.character;164}165166return new vscode.Position(line, character);167}168}169170171