Path: blob/main/extensions/merge-conflict/src/mergeDecorator.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';678export default class MergeDecorator implements vscode.Disposable {910private decorations: { [key: string]: vscode.TextEditorDecorationType } = {};1112private decorationUsesWholeLine: boolean = true; // Useful for debugging, set to false to see exact match ranges1314private config?: interfaces.IExtensionConfiguration;15private tracker: interfaces.IDocumentMergeConflictTracker;16private updating = new Map<vscode.TextEditor, boolean>();1718constructor(private context: vscode.ExtensionContext, trackerService: interfaces.IDocumentMergeConflictTrackerService) {19this.tracker = trackerService.createTracker('decorator');20}2122begin(config: interfaces.IExtensionConfiguration) {23this.config = config;24this.registerDecorationTypes(config);2526// Check if we already have a set of active windows, attempt to track these.27vscode.window.visibleTextEditors.forEach(e => this.applyDecorations(e));2829vscode.workspace.onDidOpenTextDocument(event => {30this.applyDecorationsFromEvent(event);31}, null, this.context.subscriptions);3233vscode.workspace.onDidChangeTextDocument(event => {34this.applyDecorationsFromEvent(event.document);35}, null, this.context.subscriptions);3637vscode.window.onDidChangeVisibleTextEditors((e) => {38// Any of which could be new (not just the active one).39e.forEach(e => this.applyDecorations(e));40}, null, this.context.subscriptions);41}4243configurationUpdated(config: interfaces.IExtensionConfiguration) {44this.config = config;45this.registerDecorationTypes(config);4647// Re-apply the decoration48vscode.window.visibleTextEditors.forEach(e => {49this.removeDecorations(e);50this.applyDecorations(e);51});52}5354private registerDecorationTypes(config: interfaces.IExtensionConfiguration) {5556// Dispose of existing decorations57Object.keys(this.decorations).forEach(k => this.decorations[k].dispose());58this.decorations = {};5960// None of our features are enabled61if (!config.enableDecorations || !config.enableEditorOverview) {62return;63}6465// Create decorators66if (config.enableDecorations || config.enableEditorOverview) {67this.decorations['current.content'] = vscode.window.createTextEditorDecorationType(68this.generateBlockRenderOptions('merge.currentContentBackground', 'editorOverviewRuler.currentContentForeground', config)69);7071this.decorations['incoming.content'] = vscode.window.createTextEditorDecorationType(72this.generateBlockRenderOptions('merge.incomingContentBackground', 'editorOverviewRuler.incomingContentForeground', config)73);7475this.decorations['commonAncestors.content'] = vscode.window.createTextEditorDecorationType(76this.generateBlockRenderOptions('merge.commonContentBackground', 'editorOverviewRuler.commonContentForeground', config)77);78}7980if (config.enableDecorations) {81this.decorations['current.header'] = vscode.window.createTextEditorDecorationType({82isWholeLine: this.decorationUsesWholeLine,83backgroundColor: new vscode.ThemeColor('merge.currentHeaderBackground'),84color: new vscode.ThemeColor('editor.foreground'),85outlineStyle: 'solid',86outlineWidth: '1pt',87outlineColor: new vscode.ThemeColor('merge.border'),88after: {89contentText: ' ' + vscode.l10n.t("(Current Change)"),90color: new vscode.ThemeColor('descriptionForeground')91}92});9394this.decorations['commonAncestors.header'] = vscode.window.createTextEditorDecorationType({95isWholeLine: this.decorationUsesWholeLine,96backgroundColor: new vscode.ThemeColor('merge.commonHeaderBackground'),97color: new vscode.ThemeColor('editor.foreground'),98outlineStyle: 'solid',99outlineWidth: '1pt',100outlineColor: new vscode.ThemeColor('merge.border')101});102103this.decorations['splitter'] = vscode.window.createTextEditorDecorationType({104color: new vscode.ThemeColor('editor.foreground'),105outlineStyle: 'solid',106outlineWidth: '1pt',107outlineColor: new vscode.ThemeColor('merge.border'),108isWholeLine: this.decorationUsesWholeLine,109});110111this.decorations['incoming.header'] = vscode.window.createTextEditorDecorationType({112backgroundColor: new vscode.ThemeColor('merge.incomingHeaderBackground'),113color: new vscode.ThemeColor('editor.foreground'),114outlineStyle: 'solid',115outlineWidth: '1pt',116outlineColor: new vscode.ThemeColor('merge.border'),117isWholeLine: this.decorationUsesWholeLine,118after: {119contentText: ' ' + vscode.l10n.t("(Incoming Change)"),120color: new vscode.ThemeColor('descriptionForeground')121}122});123}124}125126dispose() {127128// TODO: Replace with Map<string, T>129Object.keys(this.decorations).forEach(name => {130this.decorations[name].dispose();131});132133this.decorations = {};134}135136private generateBlockRenderOptions(backgroundColor: string, overviewRulerColor: string, config: interfaces.IExtensionConfiguration): vscode.DecorationRenderOptions {137138const renderOptions: vscode.DecorationRenderOptions = {};139140if (config.enableDecorations) {141renderOptions.backgroundColor = new vscode.ThemeColor(backgroundColor);142renderOptions.isWholeLine = this.decorationUsesWholeLine;143}144145if (config.enableEditorOverview) {146renderOptions.overviewRulerColor = new vscode.ThemeColor(overviewRulerColor);147renderOptions.overviewRulerLane = vscode.OverviewRulerLane.Full;148}149150return renderOptions;151}152153private applyDecorationsFromEvent(eventDocument: vscode.TextDocument) {154for (const editor of vscode.window.visibleTextEditors) {155if (editor.document === eventDocument) {156// Attempt to apply157this.applyDecorations(editor);158}159}160}161162private async applyDecorations(editor: vscode.TextEditor) {163if (!editor || !editor.document) { return; }164165if (!this.config || (!this.config.enableDecorations && !this.config.enableEditorOverview)) {166return;167}168169// If we have a pending scan from the same origin, exit early. (Cannot use this.tracker.isPending() because decorations are per editor.)170if (this.updating.get(editor)) {171return;172}173174try {175this.updating.set(editor, true);176177const conflicts = await this.tracker.getConflicts(editor.document);178if (vscode.window.visibleTextEditors.indexOf(editor) === -1) {179return;180}181182if (conflicts.length === 0) {183this.removeDecorations(editor);184return;185}186187// Store decorations keyed by the type of decoration, set decoration wants a "style"188// to go with it, which will match this key (see constructor);189const matchDecorations: { [key: string]: vscode.Range[] } = {};190191const pushDecoration = (key: string, d: vscode.Range) => {192matchDecorations[key] = matchDecorations[key] || [];193matchDecorations[key].push(d);194};195196conflicts.forEach(conflict => {197// TODO, this could be more effective, just call getMatchPositions once with a map of decoration to position198if (!conflict.current.decoratorContent.isEmpty) {199pushDecoration('current.content', conflict.current.decoratorContent);200}201if (!conflict.incoming.decoratorContent.isEmpty) {202pushDecoration('incoming.content', conflict.incoming.decoratorContent);203}204205conflict.commonAncestors.forEach(commonAncestorsRegion => {206if (!commonAncestorsRegion.decoratorContent.isEmpty) {207pushDecoration('commonAncestors.content', commonAncestorsRegion.decoratorContent);208}209});210211if (this.config!.enableDecorations) {212pushDecoration('current.header', conflict.current.header);213pushDecoration('splitter', conflict.splitter);214pushDecoration('incoming.header', conflict.incoming.header);215216conflict.commonAncestors.forEach(commonAncestorsRegion => {217pushDecoration('commonAncestors.header', commonAncestorsRegion.header);218});219}220});221222// For each match we've generated, apply the generated decoration with the matching decoration type to the223// editor instance. Keys in both matches and decorations should match.224Object.keys(matchDecorations).forEach(decorationKey => {225const decorationType = this.decorations[decorationKey];226227if (decorationType) {228editor.setDecorations(decorationType, matchDecorations[decorationKey]);229}230});231232} finally {233this.updating.delete(editor);234}235}236237private removeDecorations(editor: vscode.TextEditor) {238// Remove all decorations, there might be none239Object.keys(this.decorations).forEach(decorationKey => {240241// Race condition, while editing the settings, it's possible to242// generate regions before the configuration has been refreshed243const decorationType = this.decorations[decorationKey];244245if (decorationType) {246editor.setDecorations(decorationType, []);247}248});249}250}251252253