import * as vscode from 'vscode' ; import * as interfaces from './interfaces' ; import ContentProvider from './contentProvider' ; import { loadMessageBundle } from 'vscode-nls' ; const localize = loadMessageBundle ( ) ; interface IDocumentMergeConflictNavigationResults { canNavigate : boolean ; conflict ? : interfaces . IDocumentMergeConflict ; } enum NavigationDirection { Forwards , Backwards } export default class CommandHandler implements vscode . Disposable { private disposables : vscode . Disposable [ ] = [ ] ; private tracker : interfaces . IDocumentMergeConflictTracker ; constructor ( trackerService ) { this . tracker = trackerService . createTracker ( 'commands' ) ; } begin ( ) { this . disposables . push ( this . registerTextEditorCommand ( 'merge-conflict.accept.current' , this . acceptCurrent ) , this . registerTextEditorCommand ( 'merge-conflict.accept.incoming' , this . acceptIncoming ) , this . registerTextEditorCommand ( 'merge-conflict.accept.selection' , this . acceptSelection ) , this . registerTextEditorCommand ( 'merge-conflict.accept.both' , this . acceptBoth ) , this . registerTextEditorCommand ( 'merge-conflict.accept.all-current' , this . acceptAllCurrent , this . acceptAllCurrentResources ) , this . registerTextEditorCommand ( 'merge-conflict.accept.all-incoming' , this . acceptAllIncoming , this . acceptAllIncomingResources ) , this . registerTextEditorCommand ( 'merge-conflict.accept.all-both' , this . acceptAllBoth ) , this . registerTextEditorCommand ( 'merge-conflict.next' , this . navigateNext ) , this . registerTextEditorCommand ( 'merge-conflict.previous' , this . navigatePrevious ) , this . registerTextEditorCommand ( 'merge-conflict.compare' , this . compare ) ) ; } private registerTextEditorCommand ( command , cb , resourceCB ? ) { return vscode . commands . registerCommand ( command , ( ... args ) => { if ( resourceCB && args . length && args . every ( arg => arg && arg . resourceUri ) ) { return resourceCB . call ( this , args . map ( arg => arg . resourceUri ) ) ; } const editor = vscode . window . activeTextEditor ; return editor && cb . call ( this , editor , ... args ) ; } ) ; } acceptCurrent ( editor , ... args ) { return this . accept ( interfaces . CommitType . Current , editor , ... args ) ; } acceptIncoming ( editor , ... args ) { return this . accept ( interfaces . CommitType . Incoming , editor , ... args ) ; } acceptBoth ( editor , ... args ) { return this . accept ( interfaces . CommitType . Both , editor , ... args ) ; } acceptAllCurrent ( editor ) { return this . acceptAll ( interfaces . CommitType . Current , editor ) ; } acceptAllIncoming ( editor ) { return this . acceptAll ( interfaces . CommitType . Incoming , editor ) ; } acceptAllCurrentResources ( resources ) { return this . acceptAllResources ( interfaces . CommitType . Current , resources ) ; } acceptAllIncomingResources ( resources ) { return this . acceptAllResources ( interfaces . CommitType . Incoming , resources ) ; } acceptAllBoth ( editor ) { return this . acceptAll ( interfaces . CommitType . Both , editor ) ; } async compare ( editor , conflict ) { if ( ! conflict ) { conflict = await this . findConflictContainingSelection ( editor ) ; if ( ! conflict ) { vscode . window . showWarningMessage ( localize ( 'cursorNotInConflict' , 'Editor cursor is not within a merge conflict' ) ) ; return ; } } const conflicts = await this . tracker . getConflicts ( editor . document ) ; if ( ! conflicts ) { vscode . window . showWarningMessage ( localize ( 'cursorNotInConflict' , 'Editor cursor is not within a merge conflict' ) ) ; return ; } const scheme = editor . document . uri . scheme ; let range = conflict . current . content ; let leftRanges = conflicts . map ( conflict => [ conflict . current . content , conflict . range ] ) ; let rightRanges = conflicts . map ( conflict => [ conflict . incoming . content , conflict . range ] ) ; const leftUri = editor . document . uri . with ( { scheme : ContentProvider . scheme , query : JSON . stringify ( { scheme , range : range , ranges : leftRanges } ) } ) ; range = conflict . incoming . content ; const rightUri = leftUri . with ( { query : JSON . stringify ( { scheme , ranges : rightRanges } ) } ) ; let mergeConflictLineOffsets = 0 ; for ( let nextconflict of conflicts ) { if ( nextconflict . range . isEqual ( conflict . range ) ) { break ; } else { mergeConflictLineOffsets += ( nextconflict . range . end . line - nextconflict . range . start . line ) - ( nextconflict . incoming . content . end . line - nextconflict . incoming . content . start . line ) ; } } const selection = new vscode . Range ( conflict . range . start . line - mergeConflictLineOffsets , conflict . range . start . character , conflict . range . start . line - mergeConflictLineOffsets , conflict . range . start . character ) ; const docPath = editor . document . uri . path ; const fileName = docPath . substring ( docPath . lastIndexOf ( '/' ) + 1 ) ; const title = localize ( 'compareChangesTitle' , '{0}: Current Changes ⟷ Incoming Changes' , fileName ) ; const mergeConflictConfig = vscode . workspace . getConfiguration ( 'merge-conflict' ) ; const openToTheSide = mergeConflictConfig . get < string > ( 'diffViewPosition' ) ; const opts = { viewColumn : openToTheSide === 'Beside' ? vscode . ViewColumn . Beside : vscode . ViewColumn . Active , selection } ; if ( openToTheSide === 'Below' ) { await vscode . commands . executeCommand ( 'workbench.action.newGroupBelow' ) ; } await vscode . commands . executeCommand ( 'vscode.diff' , leftUri , rightUri , title , opts ) ; } navigateNext ( editor ) { return this . navigate ( editor , NavigationDirection . Forwards ) ; } navigatePrevious ( editor ) { return this . navigate ( editor , NavigationDirection . Backwards ) ; } async acceptSelection ( editor ) { let conflict = await this . findConflictContainingSelection ( editor ) ; if ( ! conflict ) { vscode . window . showWarningMessage ( localize ( 'cursorNotInConflict' , 'Editor cursor is not within a merge conflict' ) ) ; return ; } let typeToAccept ; let tokenAfterCurrentBlock = conflict . splitter ; if ( conflict . commonAncestors . length > 0 ) { tokenAfterCurrentBlock = conflict . commonAncestors [ 0 ] . header ; } if ( editor . selection . active . isBefore ( tokenAfterCurrentBlock . start ) ) { typeToAccept = interfaces . CommitType . Current ; } else if ( editor . selection . active . isAfter ( conflict . splitter . end ) ) { typeToAccept = interfaces . CommitType . Incoming ; } else if ( editor . selection . active . isBefore ( conflict . splitter . start ) ) { vscode . window . showWarningMessage ( localize ( 'cursorOnCommonAncestorsRange' , 'Editor cursor is within the common ancestors block, please move it to either the "current" or "incoming" block' ) ) ; return ; } else { vscode . window . showWarningMessage ( localize ( 'cursorOnSplitterRange' , 'Editor cursor is within the merge conflict splitter, please move it to either the "current" or "incoming" block' ) ) ; return ; } this . tracker . forget ( editor . document ) ; conflict . commitEdit ( typeToAccept , editor ) ; } dispose ( ) { this . disposables . forEach ( disposable => disposable . dispose ( ) ) ; this . disposables = [ ] ; } private async navigate ( editor , direction ) { let navigationResult = await this . findConflictForNavigation ( editor , direction ) ; if ( ! navigationResult ) { const mergeConflictConfig = vscode . workspace . getConfiguration ( 'merge-conflict' ) ; if ( mergeConflictConfig . get < boolean > ( 'autoNavigateNextConflict.enabled' ) ) { return ; } vscode . window . showWarningMessage ( localize ( 'noConflicts' , 'No merge conflicts found in this file' ) ) ; return ; } else if ( ! navigationResult . canNavigate ) { vscode . window . showWarningMessage ( localize ( 'noOtherConflictsInThisFile' , 'No other merge conflicts within this file' ) ) ; return ; } else if ( ! navigationResult . conflict ) { 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 ; if ( args [ 0 ] === 'known-conflict' ) { conflict = args [ 1 ] ; } else { conflict = await this . findConflictContainingSelection ( editor ) ; } if ( ! conflict ) { vscode . window . showWarningMessage ( localize ( 'cursorNotInConflict' , 'Editor cursor is not within a merge conflict' ) ) ; return ; } this . tracker . forget ( editor . document ) ; conflict . commitEdit ( type , editor ) ; const mergeConflictConfig = vscode . workspace . getConfiguration ( 'merge-conflict' ) ; if ( mergeConflictConfig . get < boolean > ( 'autoNavigateNextConflict.enabled' ) ) { this . navigateNext ( editor ) ; } } private async acceptAll ( type , editor ) { let conflicts = await this . tracker . getConflicts ( editor . document ) ; if ( ! conflicts || conflicts . length === 0 ) { vscode . window . showWarningMessage ( localize ( 'noConflicts' , 'No merge conflicts found in this file' ) ) ; return ; } this . tracker . forget ( editor . document ) ; await editor . edit ( ( edit ) => conflicts . forEach ( conflict => { conflict . applyEdit ( type , editor . document , edit ) ; } ) ) ; } private async acceptAllResources ( type , resources ) { const documents = await Promise . all ( resources . map ( resource => vscode . workspace . openTextDocument ( resource ) ) ) ; const edit = new vscode . WorkspaceEdit ( ) ; for ( const document of documents ) { const conflicts = await this . tracker . getConflicts ( document ) ; if ( ! conflicts || conflicts . length === 0 ) { continue ; } this . tracker . forget ( document ) ; conflicts . forEach ( conflict => { conflict . applyEdit ( type , document , { replace : ( range , newText ) => edit . replace ( document . uri , range , newText ) } ) ; } ) ; } vscode . workspace . applyEdit ( edit ) ; } private async findConflictContainingSelection ( editor , conflicts ? ) { if ( ! conflicts ) { conflicts = await this . tracker . getConflicts ( editor . document ) ; } if ( ! conflicts || conflicts . length === 0 ) { return null ; } for ( const conflict of conflicts ) { if ( conflict . range . contains ( editor . selection . active ) ) { return conflict ; } } 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 ; let fallback ; let scanOrder ; if ( direction === NavigationDirection . Forwards ) { predicate = ( conflict ) => selection . isBefore ( conflict . range . start ) ; fallback = ( ) => conflicts ! [ 0 ] ; scanOrder = conflicts ; } else if ( direction === NavigationDirection . Backwards ) { predicate = ( conflict ) => selection . isAfter ( conflict . range . start ) ; fallback = ( ) => conflicts ! [ conflicts ! . length - 1 ] ; scanOrder = conflicts . slice ( ) . reverse ( ) ; } else { throw new Error ( `Unsupported direction ${ direction }` ) ; } for ( const conflict of scanOrder ) { if ( predicate ( conflict ) && ! conflict . range . contains ( selection ) ) { return { canNavigate : true , conflict : conflict } ; } } return { canNavigate : true , conflict : fallback ( ) } ; } }