diff --git a/package.json b/package.json index 47f56932..413613ee 100644 --- a/package.json +++ b/package.json @@ -75,10 +75,15 @@ "group": "navigation@3" }, { - "command": "todo-tree.scanWorkspaceAndOpenFiles", + "command": "todo-tree.scanGitBranch", "when": "view =~ /todo-tree/ && todo-tree-scan-mode == 'current file' && todo-tree-show-scan-mode-button == true", "group": "navigation@3" }, + { + "command": "todo-tree.scanWorkspaceAndOpenFiles", + "when": "view =~ /todo-tree/ && todo-tree-scan-mode == 'git branch' && todo-tree-show-scan-mode-button == true", + "group": "navigation@3" + }, { "command": "todo-tree.scanWorkspaceOnly", "when": "view =~ /todo-tree/ && todo-tree-scan-mode == 'workspace' && todo-tree-show-scan-mode-button == true", @@ -221,6 +226,11 @@ "when": "view =~ /todo-tree/ && todo-tree-scan-mode != 'workspace only'", "group": "3-view" }, + { + "command": "todo-tree.scanGitBranch", + "when": "view =~ /todo-tree/ && todo-tree-scan-mode != 'git branch'", + "group": "3-view" + }, { "command": "todo-tree.expand", "when": "view =~ /todo-tree/ && todo-tree-expanded == false", @@ -395,6 +405,12 @@ "category": "%todo-tree.command.category%", "icon": "$(folder)" }, + { + "command": "todo-tree.scanGitBranch", + "title": "%todo-tree.command.scanGitBranch.title%", + "category": "%todo-tree.command.category%", + "icon": "$(git-branch)" + }, { "command": "todo-tree.addTag", "title": "%todo-tree.command.addTag.title%", @@ -1005,14 +1021,16 @@ "workspace", "open files", "current file", - "workspace only" + "workspace only", + "git branch" ], "markdownDescription": "%todo-tree.configuration.tree.scanMode.markdownDescription%", "markdownEnumDescriptions": [ "%todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.1%", "%todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.2%", "%todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.3%", - "%todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.4%" + "%todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.4%", + "%todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.5%" ], "type": "string" }, diff --git a/package.nls.json b/package.nls.json index 57b54f97..fdcb32fe 100644 --- a/package.nls.json +++ b/package.nls.json @@ -18,6 +18,7 @@ "todo-tree.command.scanCurrentFileOnly.title": "Scan Current File Only", "todo-tree.command.scanWorkspaceAndOpenFiles.title": "Scan Workspace And Open Files", "todo-tree.command.scanWorkspaceOnly.title": "Scan Workspace Only", + "todo-tree.command.scanGitBranch.title": "Scan Git Branch", "todo-tree.command.addTag.title": "Add Tag", "todo-tree.command.removeTag.title": "Remove Tag", "todo-tree.command.exportTree.title": "Export Tree", @@ -113,6 +114,7 @@ "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.2": "Scan open files only", "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.3": "Scan the current file only", "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.4": "Scan the workspace but don't refresh files open in the editor", + "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.5": "Scan only TODOs added in the current git branch (compared to main or master)", "todo-tree.configuration.tree.showBadges.markdownDescription": "Show badges and SCM state in the tree view.", "todo-tree.configuration.tree.showCountsInTree.markdownDescription": "Show counts of TODOs in the tree.", "todo-tree.configuration.tree.showInExplorer.deprecationMessage": "This setting is no longer used. Please drag the view to move it.", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index bd861aac..9f396e5c 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -16,6 +16,7 @@ "todo-tree.command.scanCurrentFileOnly.title": "仅扫描当前文件", "todo-tree.command.scanWorkspaceAndOpenFiles.title": "扫描整个工作区和打开的文件", "todo-tree.command.scanWorkspaceOnly.title": "仅扫描整个工作区", + "todo-tree.command.scanGitBranch.title": "扫描 Git 分支", "todo-tree.command.addTag.title": "添加标签类型", "todo-tree.command.removeTag.title": "移除标签类型", "todo-tree.command.exportTree.title": "导出树状图", @@ -98,6 +99,7 @@ "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.2": "仅扫描打开的文件。", "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.3": "仅扫描当前文件。", "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.4": "仅扫描整个工作区(不包括打开的文件)。", + "todo-tree.configuration.tree.scanMode.markdownEnumDescriptions.5": "仅扫描当前 Git 分支中添加的待办事项(与 main 或 master 分支比较)。", "todo-tree.configuration.tree.showBadges.markdownDescription": "在树状图中显示 badges 和 SCM 的状态。", "todo-tree.configuration.tree.showCountsInTree.markdownDescription": "在树状图中显示待办事项的数目。", "todo-tree.configuration.tree.showInExplorer.deprecationMessage": "该设置不再使用,请拖动视图来进行移动。", diff --git a/src/extension.js b/src/extension.js index 5c7bf02a..53354a7f 100644 --- a/src/extension.js +++ b/src/extension.js @@ -36,6 +36,7 @@ var SCAN_MODE_WORKSPACE_AND_OPEN_FILES = 'workspace'; var SCAN_MODE_OPEN_FILES = 'open files'; var SCAN_MODE_CURRENT_FILE = 'current file'; var SCAN_MODE_WORKSPACE_ONLY = 'workspace only'; +var SCAN_MODE_GIT_BRANCH = 'git branch'; var STATUS_BAR_TOTAL = 'total'; var STATUS_BAR_TAGS = 'tags'; @@ -274,6 +275,10 @@ function activate( context ) { statusBarIndicator.text += " (in current file)"; } + else if( scanMode === SCAN_MODE_GIT_BRANCH ) + { + statusBarIndicator.text += " (in git branch)"; + } statusBarIndicator.command = "todo-tree.onStatusBarClicked"; } @@ -350,6 +355,130 @@ function activate( context ) } ); } + function getBaseBranch( workspaceFolder ) + { + return new Promise( function( resolve, reject ) + { + // First try 'main' + child_process.exec( "git rev-parse --verify main", { cwd: workspaceFolder }, ( err, stdout, stderr ) => + { + if( !err ) + { + debug( "Using 'main' as base branch" ); + resolve( 'main' ); + } + else + { + // Fall back to 'master' + child_process.exec( "git rev-parse --verify master", { cwd: workspaceFolder }, ( err, stdout, stderr ) => + { + if( !err ) + { + debug( "Using 'master' as base branch" ); + resolve( 'master' ); + } + else + { + debug( "No main or master branch found" ); + reject( new Error( "No 'main' or 'master' branch found" ) ); + } + } ); + } + } ); + } ); + } + + function searchGitBranch( workspaceFolder ) + { + return getBaseBranch( workspaceFolder ).then( function( baseBranch ) + { + return new Promise( function( resolve, reject ) + { + debug( "Running git diff " + baseBranch + "..HEAD in " + workspaceFolder ); + + child_process.exec( "git diff " + baseBranch + "..HEAD --unified=0", { cwd: workspaceFolder, maxBuffer: 50 * 1024 * 1024 }, ( err, stdout, stderr ) => + { + if( err ) + { + debug( "Git diff error: " + stderr ); + reject( new Error( stderr || err.message ) ); + return; + } + + var diffOutput = stdout.toString(); + if( !diffOutput.trim() ) + { + debug( "No diff found between " + baseBranch + " and HEAD" ); + resolve(); + return; + } + + var regex = utils.getRegexForEditorSearch( true ); + var lines = diffOutput.split( '\n' ); + var currentFile = null; + var currentLineNumber = 0; + + for( var i = 0; i < lines.length; i++ ) + { + var line = lines[ i ]; + + // Parse file header: +++ b/path/to/file + if( line.startsWith( '+++ b/' ) ) + { + currentFile = line.substring( 6 ); + debug( "Diff file: " + currentFile ); + continue; + } + + // Parse hunk header: @@ -old,count +new,count @@ + var hunkMatch = line.match( /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/ ); + if( hunkMatch ) + { + currentLineNumber = parseInt( hunkMatch[ 1 ], 10 ); + continue; + } + + // Process added lines (start with + but not +++) + if( line.startsWith( '+' ) && !line.startsWith( '+++' ) ) + { + var addedContent = line.substring( 1 ); + + // Check if this line matches our TODO regex + regex.lastIndex = 0; + var match = regex.exec( addedContent ); + if( match && currentFile ) + { + var fsPath = path.join( workspaceFolder, currentFile ); + var result = { + fsPath: fsPath, + uri: vscode.Uri.file( fsPath ), + line: currentLineNumber, + column: match.index + 1, + match: addedContent + }; + debug( " Match (Git Branch): " + JSON.stringify( result ) ); + searchResults.add( result ); + } + currentLineNumber++; + } + else if( !line.startsWith( '-' ) && !line.startsWith( '\\' ) && line.length > 0 ) + { + // Context line (no prefix) - shouldn't happen with --unified=0 but handle anyway + currentLineNumber++; + } + // Removed lines (start with -) don't affect line numbering for new file + } + + resolve(); + } ); + } ); + } ).catch( function( err ) + { + vscode.window.showWarningMessage( "Todo-Tree: " + err.message ); + return Promise.resolve(); + } ); + } + function addGlobs( source, target, exclude ) { Object.keys( source ).map( function( glob ) @@ -470,7 +599,8 @@ function activate( context ) function refreshOpenFiles() { - if( config.scanMode() !== SCAN_MODE_WORKSPACE_ONLY ) + var scanMode = config.scanMode(); + if( scanMode !== SCAN_MODE_WORKSPACE_ONLY && scanMode !== SCAN_MODE_GIT_BRANCH ) { Object.keys( openDocuments ).map( function( document ) { @@ -586,26 +716,48 @@ function activate( context ) statusBarIndicator.command = "todo-tree.stopScan"; statusBarIndicator.tooltip = "Click to interrupt scan"; - searchList = getRootFolders(); - - if( searchList.length === 0 ) - { - searchWorkspaces( searchList ); - } + var scanMode = config.scanMode(); - if( config.shouldIgnoreGitSubmodules() ) + if( scanMode === SCAN_MODE_GIT_BRANCH ) { - submoduleExcludeGlobs = []; - searchList.forEach( function( rootPath ) + // For git branch mode, search git diffs in each workspace folder + var workspaceFolders = vscode.workspace.workspaceFolders || []; + var gitSearchPromises = workspaceFolders.map( function( folder ) { - submoduleExcludeGlobs = submoduleExcludeGlobs.concat( utils.getSubmoduleExcludeGlobs( rootPath ) ); + return searchGitBranch( folder.uri.fsPath ); } ); - context.workspaceState.update( 'submoduleExcludeGlobs', submoduleExcludeGlobs ); + + Promise.all( gitSearchPromises ) + .then( function() + { + debug( "Found " + searchResults.count() + " items in git branch diff" ); + addResultsToTree(); + setButtonsAndContext(); + } ); } + else + { + searchList = getRootFolders(); + + if( searchList.length === 0 ) + { + searchWorkspaces( searchList ); + } + + if( config.shouldIgnoreGitSubmodules() ) + { + submoduleExcludeGlobs = []; + searchList.forEach( function( rootPath ) + { + submoduleExcludeGlobs = submoduleExcludeGlobs.concat( utils.getSubmoduleExcludeGlobs( rootPath ) ); + } ); + context.workspaceState.update( 'submoduleExcludeGlobs', submoduleExcludeGlobs ); + } - iterateSearchList() - .finally( refreshOpenFiles ) - .then( addResultsToTree ); + iterateSearchList() + .finally( refreshOpenFiles ) + .then( addResultsToTree ); + } } function triggerRescan() @@ -970,6 +1122,11 @@ function activate( context ) vscode.workspace.getConfiguration( 'todo-tree.tree' ).update( 'scanMode', SCAN_MODE_WORKSPACE_ONLY, vscode.ConfigurationTarget.Workspace ); } + function scanGitBranch() + { + vscode.workspace.getConfiguration( 'todo-tree.tree' ).update( 'scanMode', SCAN_MODE_GIT_BRANCH, vscode.ConfigurationTarget.Workspace ); + } + function dumpFolderFilter() { debug( "Folder filter include:" + JSON.stringify( context.workspaceState.get( 'includeGlobs' ) ) ); @@ -1254,7 +1411,8 @@ function activate( context ) function shouldRefreshFile() { - return vscode.workspace.getConfiguration( 'todo-tree.tree' ).autoRefresh === true && config.scanMode() !== SCAN_MODE_WORKSPACE_ONLY; + var scanMode = config.scanMode(); + return vscode.workspace.getConfiguration( 'todo-tree.tree' ).autoRefresh === true && scanMode !== SCAN_MODE_WORKSPACE_ONLY && scanMode !== SCAN_MODE_GIT_BRANCH; } // We can't do anything if we can't find ripgrep @@ -1693,6 +1851,7 @@ function activate( context ) context.subscriptions.push( vscode.commands.registerCommand( 'todo-tree.scanOpenFilesOnly', scanOpenFilesOnly ) ); context.subscriptions.push( vscode.commands.registerCommand( 'todo-tree.scanCurrentFileOnly', scanCurrentFileOnly ) ); context.subscriptions.push( vscode.commands.registerCommand( 'todo-tree.scanWorkspaceOnly', scanWorkspaceOnly ) ); + context.subscriptions.push( vscode.commands.registerCommand( 'todo-tree.scanGitBranch', scanGitBranch ) ); context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor( function( e ) { @@ -1764,11 +1923,12 @@ function activate( context ) delete openDocuments[ document.uri.toString() ]; - if( vscode.workspace.getConfiguration( 'todo-tree.tree' ).autoRefresh === true && config.scanMode() !== SCAN_MODE_WORKSPACE_ONLY ) + var scanMode = config.scanMode(); + if( vscode.workspace.getConfiguration( 'todo-tree.tree' ).autoRefresh === true && scanMode !== SCAN_MODE_WORKSPACE_ONLY && scanMode !== SCAN_MODE_GIT_BRANCH ) { if( config.isValidScheme( document.uri ) ) { - if( config.scanMode() !== SCAN_MODE_WORKSPACE_AND_OPEN_FILES ) + if( scanMode !== SCAN_MODE_WORKSPACE_AND_OPEN_FILES ) { removeFromTree( document.uri ); }