import * as vscode from 'vscode' ; import * as interfaces from './interfaces' ; import ContentProvider from './contentProvider' ; import * as path from 'path' ; const messages = { cursorNotInConflict : 'Editor cursor is not within a merge conflict' , cursorOnSplitterRange : 'Editor cursor is within the merge conflict splitter, please move it to either the "current" or "incoming" block' , noConflicts : 'No merge conflicts found in this file' , noOtherConflictsInThisFile : 'No other merge conflicts within this file' } ; interface IDocumentMergeConflictNavigationResults { canNavigate : boolean ; conflict ? : interfaces . IDocumentMergeConflict ; } enum NavigationDirection { Forwards , Backwards } export default class CommandHandler implements vscode . Disposable { private disposables : vscode . Disposable [ ] = [ ] ; constructor ( private context , private tracker ) { } begin ( ) { this . disposables . push ( vscode . commands . registerTextEditorCommand ( 'better-merge.accept.current' , this . acceptCurrent , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.accept.incoming' , this . acceptIncoming , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.accept.selection' , this . acceptSelection , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.accept.both' , this . acceptBoth , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.accept.all-current' , this . acceptAllCurrent , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.accept.all-incoming' , this . acceptAllIncoming , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.accept.all-both' , this . acceptAllBoth , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.next' , this . navigateNext , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.previous' , this . navigatePrevious , this ) , vscode . commands . registerTextEditorCommand ( 'better-merge.compare' , this . compare , this ) ) ; } acceptCurrent ( editor , edit , ... args ) { return this . accept ( interfaces . CommitType . Current , editor , ... args ) ; } acceptIncoming ( editor , edit , ... args ) { return this . accept ( interfaces . CommitType . Incoming , editor , ... args ) ; } acceptBoth ( editor , edit , ... args ) { return this . accept ( interfaces . CommitType . Both , editor , ... args ) ; } acceptAllCurrent ( editor , edit , ... args ) { return this . acceptAll ( interfaces . CommitType . Current , editor ) ; } acceptAllIncoming ( editor , edit , ... args ) { return this . acceptAll ( interfaces . CommitType . Incoming , editor ) ; } acceptAllBoth ( editor , edit , ... args ) { return this . acceptAll ( interfaces . CommitType . Both , editor ) ; } async compare ( editor , edit , conflict , ... args ) { const fileName = path . basename ( editor . document . uri . fsPath ) ; if ( ! conflict ) { conflict = await this . findConflictContainingSelection ( editor ) ; if ( ! conflict ) { vscode . window . showWarningMessage ( messages . cursorNotInConflict ) ; return ; } } let range = conflict . current . content ; const leftUri = editor . document . uri . with ( { scheme : ContentProvider . scheme , query : JSON . stringify ( range ) } ) ; const leftTitle = `Current changes` ; range = conflict . incoming . content ; const rightUri = leftUri . with ( { query : JSON . stringify ( range ) } ) ; const rightTitle = `Incoming changes` ; const title = `${ fileName }: ${ leftTitle } \u2194 ${ rightTitle }` ; vscode . commands . executeCommand ( 'vscode.diff' , leftUri , rightUri , title ) ; } navigateNext ( editor , edit , ... args ) { return this . navigate ( editor , NavigationDirection . Forwards ) ; } navigatePrevious ( editor , edit , ... args ) { return this . navigate ( editor , NavigationDirection . Backwards ) ; } async acceptSelection ( editor , edit , ... args ) { let conflict = await this . findConflictContainingSelection ( editor ) ; if ( ! conflict ) { vscode . window . showWarningMessage ( messages . cursorNotInConflict ) ; return ; } let typeToAccept = null ; if ( editor . selection . active . isBefore ( conflict . splitter . start ) ) { typeToAccept = interfaces . CommitType . Current ; } else if ( editor . selection . active . isAfter ( conflict . splitter . end ) ) { typeToAccept = interfaces . CommitType . Incoming ; } else { vscode . window . showWarningMessage ( messages . cursorOnSplitterRange ) ; return ; } this . tracker . forget ( editor . document ) ; conflict . commitEdit ( typeToAccept , editor ) ; } dispose ( ) { if ( this . disposables ) { this . disposables . forEach ( disposable => disposable . dispose ( ) ) ; this . disposables = null ; } } private async navigate ( editor , direction ) { let navigationResult = await this . findConflictForNavigation ( editor , direction ) ; if ( ! navigationResult ) { vscode . window . showWarningMessage ( messages . noConflicts ) ; return ; } else if ( ! navigationResult . canNavigate ) { vscode . window . showWarningMessage ( messages . noOtherConflictsInThisFile ) ; return ; } editor . selection = new vscode . Selection ( navigationResult . conflict . range . start , navigationResult . conflict . range . start ) ; editor . revealRange ( navigationResult . conflict . range , vscode . TextEditorRevealType . Default ) ; } private async accept ( type , editor , ... args ) { let conflict = null ; if ( args [ 0 ] === 'known-conflict' ) { conflict = args [ 1 ] ; } else { conflict = await this . findConflictContainingSelection ( editor ) ; } if ( ! conflict ) { vscode . window . showWarningMessage ( messages . cursorNotInConflict ) ; return ; } this . tracker . forget ( editor . document ) ; conflict . commitEdit ( type , editor ) ; } private async acceptAll ( type , editor ) { let conflicts = await this . tracker . getConflicts ( editor . document ) ; if ( ! conflicts || conflicts . length === 0 ) { vscode . window . showWarningMessage ( messages . noConflicts ) ; return ; } this . tracker . forget ( editor . document ) ; await editor . edit ( ( edit ) => conflicts . forEach ( conflict => { conflict . applyEdit ( type , editor , edit ) ; } ) ) ; } private async findConflictContainingSelection ( editor , conflicts ? ) { if ( ! conflicts ) { conflicts = await this . tracker . getConflicts ( editor . document ) ; } if ( ! conflicts || conflicts . length === 0 ) { return null ; } for ( let i = 0 ; i < conflicts . length ; i ++ ) { if ( conflicts [ i ] . range . contains ( editor . selection . active ) ) { return conflicts [ i ] ; } } return null ; } private async findConflictForNavigation ( editor , direction , conflicts ? ) { if ( ! conflicts ) { conflicts = await this . tracker . getConflicts ( editor . document ) ; } if ( ! conflicts || conflicts . length === 0 ) { return null ; } let selection = editor . selection . active ; if ( conflicts . length === 1 ) { if ( conflicts [ 0 ] . range . contains ( selection ) ) { return { canNavigate : false } ; } return { canNavigate : true , conflict : conflicts [ 0 ] } ; } let predicate = null ; let fallback = null ; if ( direction === NavigationDirection . Forwards ) { predicate = ( conflict ) => selection . isBefore ( conflict . range . start ) ; fallback = ( ) => conflicts [ 0 ] ; } else if ( direction === NavigationDirection . Backwards ) { predicate = ( conflict ) => selection . isAfter ( conflict . range . start ) ; fallback = ( ) => conflicts [ conflicts . length - 1 ] ; } else { throw new Error ( `Unsupported direction ${ direction }` ) ; } for ( let i = 0 ; i < conflicts . length ; i ++ ) { if ( predicate ( conflicts [ i ] ) && ! conflicts [ i ] . range . contains ( selection ) ) { return { canNavigate : true , conflict : conflicts [ i ] } ; } } return { canNavigate : true , conflict : fallback ( ) } ; } }