Skip to content

Commit 1f2bb6e

Browse files
authored
Add "Mark as viewed/unviewed" to editor toolbar (#3420)
1 parent c4d2ea0 commit 1f2bb6e

10 files changed

+107
-20
lines changed

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,16 @@
13941394
"command": "issue.createIssueFromFile",
13951395
"group": "navigation",
13961396
"when": "resourceFilename == NewIssue.md"
1397+
},
1398+
{
1399+
"command": "pr.markFileAsViewed",
1400+
"group": "navigation",
1401+
"when": "resourceScheme != pr && resourceScheme != review && resourceScheme != filechange && resourcePath in github:unviewedFiles"
1402+
},
1403+
{
1404+
"command": "pr.unmarkFileAsViewed",
1405+
"group": "navigation",
1406+
"when": "resourceScheme != pr && resourceScheme != review && resourceScheme != filechange && resourcePath in github:viewedFiles"
13971407
}
13981408
],
13991409
"scm/title": [

src/commands.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { CategoryTreeNode } from './view/treeNodes/categoryNode';
3030
import { CommitNode } from './view/treeNodes/commitNode';
3131
import { DescriptionNode } from './view/treeNodes/descriptionNode';
3232
import {
33+
FileChangeNode,
3334
GitFileChangeNode,
3435
InMemFileChangeNode,
3536
openFileCommand,
@@ -853,19 +854,39 @@ export function registerCommands(
853854
);
854855

855856
context.subscriptions.push(
856-
vscode.commands.registerCommand('pr.markFileAsViewed', async (treeNode: GitFileChangeNode) => {
857+
vscode.commands.registerCommand('pr.markFileAsViewed', async (treeNode: FileChangeNode | vscode.Uri) => {
857858
try {
858-
await treeNode.pullRequest.markFileAsViewed(treeNode.fileName);
859+
if (treeNode instanceof FileChangeNode) {
860+
await treeNode.pullRequest.markFileAsViewed(treeNode.fileName);
861+
const manager = reposManager.getManagerForFile(treeNode.filePath);
862+
if (treeNode.pullRequest === manager?.activePullRequest) {
863+
treeNode.pullRequest.setFileViewedContext();
864+
}
865+
} else {
866+
const manager = reposManager.getManagerForFile(treeNode);
867+
await manager?.activePullRequest?.markFileAsViewed(treeNode.path);
868+
manager?.activePullRequest?.setFileViewedContext();
869+
}
859870
} catch (e) {
860871
vscode.window.showErrorMessage(`Marked file as viewed failed: ${e}`);
861872
}
862873
}),
863874
);
864875

865876
context.subscriptions.push(
866-
vscode.commands.registerCommand('pr.unmarkFileAsViewed', async (treeNode: GitFileChangeNode) => {
877+
vscode.commands.registerCommand('pr.unmarkFileAsViewed', async (treeNode: FileChangeNode | vscode.Uri) => {
867878
try {
868-
await treeNode.pullRequest.unmarkFileAsViewed(treeNode.fileName);
879+
if (treeNode instanceof FileChangeNode) {
880+
await treeNode.pullRequest.unmarkFileAsViewed(treeNode.fileName);
881+
const manager = reposManager.getManagerForFile(treeNode.filePath);
882+
if (treeNode.pullRequest === manager?.activePullRequest) {
883+
treeNode.pullRequest.setFileViewedContext();
884+
}
885+
} else {
886+
const manager = reposManager.getManagerForFile(treeNode);
887+
await manager?.activePullRequest?.unmarkFileAsViewed(treeNode.path);
888+
manager?.activePullRequest?.setFileViewedContext();
889+
}
869890
} catch (e) {
870891
vscode.window.showErrorMessage(`Marked file as not viewed failed: ${e}`);
871892
}

src/common/executeCommands.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,21 @@
55

66
import * as vscode from 'vscode';
77

8+
export namespace contexts {
9+
export const VIEWED_FILES = 'github:viewedFiles';
10+
export const UNVIEWED_FILES = 'github:unviewedFiles';
11+
}
12+
813
export namespace commands {
9-
export function executeCommand(command: string) {
10-
return vscode.commands.executeCommand(command);
14+
export function executeCommand(command: string, arg1?: any, arg2?: any) {
15+
return vscode.commands.executeCommand(command, arg1, arg2);
1116
}
1217

1318
export function focusView(viewId: string) {
1419
return executeCommand(`${viewId}.focus`);
1520
}
21+
22+
export function setContext(context: string, value: any) {
23+
return executeCommand('setContext', context, value);
24+
}
1625
}

src/github/folderRepositoryManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1894,7 +1894,7 @@ export class FolderRepositoryManager implements vscode.Disposable {
18941894
}
18951895

18961896
private createAndAddGitHubRepository(remote: Remote, credentialStore: CredentialStore) {
1897-
const repo = new GitHubRepository(remote, credentialStore, this.telemetry, this._sessionState);
1897+
const repo = new GitHubRepository(remote, this.repository.rootUri, credentialStore, this.telemetry, this._sessionState);
18981898
this._githubRepositories.push(repo);
18991899
return repo;
19001900
}

src/github/githubRepository.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export class GitHubRepository implements vscode.Disposable {
148148

149149
constructor(
150150
public remote: Remote,
151+
public readonly rootUri: vscode.Uri,
151152
private readonly _credentialStore: CredentialStore,
152153
private readonly _telemetry: ITelemetry,
153154
private readonly _sessionState: ISessionState
@@ -410,7 +411,7 @@ export class GitHubRepository implements vscode.Disposable {
410411
parsedIssue.repositoryUrl,
411412
new Protocol(parsedIssue.repositoryUrl),
412413
);
413-
githubRepository = new GitHubRepository(remote, this._credentialStore, this._telemetry, this._sessionState);
414+
githubRepository = new GitHubRepository(remote, this.rootUri, this._credentialStore, this._telemetry, this._sessionState);
414415
}
415416
return githubRepository;
416417
}

src/github/pullRequestModel.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as vscode from 'vscode';
1010
import { Repository } from '../api/api';
1111
import { DiffSide, IComment, IReviewThread, ViewedState } from '../common/comment';
1212
import { parseDiff } from '../common/diffHunk';
13+
import { commands, contexts } from '../common/executeCommands';
1314
import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file';
1415
import { GitHubRef } from '../common/githubRef';
1516
import Logger from '../common/logger';
@@ -107,6 +108,8 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
107108
public onDidChangeReviewThreads = this._onDidChangeReviewThreads.event;
108109

109110
private _fileChangeViewedState: FileViewedState = {};
111+
private _viewedFiles: Set<string> = new Set();
112+
private _unviewedFiles: Set<string> = new Set();
110113
private _onDidChangeFileViewedState = new vscode.EventEmitter<FileViewedStateChangeEvent>();
111114
public onDidChangeFileViewedState = this._onDidChangeFileViewedState.event;
112115

@@ -1388,8 +1391,9 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
13881391
if (this._fileChangeViewedState[n.path] !== n.viewerViewedState) {
13891392
changed.push({ fileName: n.path, viewed: n.viewerViewedState });
13901393
}
1391-
1392-
this._fileChangeViewedState[n.path] = n.viewerViewedState;
1394+
// No event for setting the file viewed state here.
1395+
// Instead, wait until all the changes have been made and set the context at the end.
1396+
this.setFileViewedState(n.path, n.viewerViewedState, false);
13931397
});
13941398

13951399
hasNextPage = data.repository.pullRequest.files.pageInfo.hasNextPage;
@@ -1401,8 +1405,10 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
14011405
}
14021406
}
14031407

1404-
async markFileAsViewed(fileName: string): Promise<void> {
1408+
async markFileAsViewed(filePathOrSubpath: string): Promise<void> {
14051409
const { mutate, schema } = await this.githubRepository.ensure();
1410+
const fileName = filePathOrSubpath.startsWith(this.githubRepository.rootUri.path) ?
1411+
filePathOrSubpath.substring(this.githubRepository.rootUri.path.length + 1) : filePathOrSubpath;
14061412
await mutate<void>({
14071413
mutation: schema.MarkFileAsViewed,
14081414
variables: {
@@ -1413,12 +1419,13 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
14131419
},
14141420
});
14151421

1416-
this._fileChangeViewedState[fileName] = ViewedState.VIEWED;
1417-
this._onDidChangeFileViewedState.fire({ changed: [{ fileName, viewed: ViewedState.VIEWED }] });
1422+
this.setFileViewedState(fileName, ViewedState.VIEWED, true);
14181423
}
14191424

1420-
async unmarkFileAsViewed(fileName: string): Promise<void> {
1425+
async unmarkFileAsViewed(filePathOrSubpath: string): Promise<void> {
14211426
const { mutate, schema } = await this.githubRepository.ensure();
1427+
const fileName = filePathOrSubpath.startsWith(this.githubRepository.rootUri.path) ?
1428+
filePathOrSubpath.substring(this.githubRepository.rootUri.path.length + 1) : filePathOrSubpath;
14221429
await mutate<void>({
14231430
mutation: schema.UnmarkFileAsViewed,
14241431
variables: {
@@ -1429,7 +1436,39 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
14291436
},
14301437
});
14311438

1432-
this._fileChangeViewedState[fileName] = ViewedState.UNVIEWED;
1433-
this._onDidChangeFileViewedState.fire({ changed: [{ fileName, viewed: ViewedState.UNVIEWED }] });
1439+
this.setFileViewedState(fileName, ViewedState.UNVIEWED, true);
1440+
}
1441+
1442+
private setFileViewedState(fileSubpath: string, viewedState: ViewedState, event: boolean) {
1443+
const filePath = vscode.Uri.joinPath(this.githubRepository.rootUri, fileSubpath).fsPath;
1444+
switch (viewedState) {
1445+
case ViewedState.DISMISSED: {
1446+
this._viewedFiles.delete(filePath);
1447+
this._unviewedFiles.delete(filePath);
1448+
break;
1449+
}
1450+
case ViewedState.UNVIEWED: {
1451+
this._viewedFiles.delete(filePath);
1452+
this._unviewedFiles.add(filePath);
1453+
break;
1454+
}
1455+
case ViewedState.VIEWED: {
1456+
this._viewedFiles.add(filePath);
1457+
this._unviewedFiles.delete(filePath);
1458+
}
1459+
}
1460+
this._fileChangeViewedState[fileSubpath] = viewedState;
1461+
if (event) {
1462+
this._onDidChangeFileViewedState.fire({ changed: [{ fileName: fileSubpath, viewed: viewedState }] });
1463+
}
1464+
}
1465+
1466+
/**
1467+
* Using these contexts is fragile in a multi-root workspace where multiple PRs are checked out.
1468+
* If you have two active PRs that have the same file path relative to their rootdir, then these context can get confused.
1469+
*/
1470+
public setFileViewedContext() {
1471+
commands.setContext(contexts.VIEWED_FILES, Array.from(this._viewedFiles));
1472+
commands.setContext(contexts.UNVIEWED_FILES, Array.from(this._unviewedFiles));
14341473
}
14351474
}

src/test/github/folderRepositoryManager.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { GitApiImpl } from '../../api/api1';
1515
import { CredentialStore } from '../../github/credentials';
1616
import { MockExtensionContext } from '../mocks/mockExtensionContext';
1717
import { MockSessionState } from '../mocks/mockSessionState';
18+
import { Uri } from 'vscode';
1819

1920
describe('PullRequestManager', function () {
2021
let sinon: SinonSandbox;
@@ -46,7 +47,8 @@ describe('PullRequestManager', function () {
4647
const url = 'https://github.com/aaa/bbb.git';
4748
const protocol = new Protocol(url);
4849
const remote = new Remote('origin', url, protocol);
49-
const repository = new GitHubRepository(remote, manager.credentialStore, telemetry, new MockSessionState());
50+
const rootUri = Uri.file('C:\\users\\test\\repo');
51+
const repository = new GitHubRepository(remote, rootUri, manager.credentialStore, telemetry, new MockSessionState());
5052
const prItem = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().build(), repository);
5153
const pr = new PullRequestModel(telemetry, repository, remote, prItem);
5254

src/test/github/githubRepository.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Remote } from '../../common/remote';
77
import { Protocol } from '../../common/protocol';
88
import { GitHubRepository } from '../../github/githubRepository';
99
import { MockSessionState } from '../mocks/mockSessionState';
10+
import { Uri } from 'vscode';
1011

1112
describe('GitHubRepository', function () {
1213
let sinon: SinonSandbox;
@@ -29,14 +30,16 @@ describe('GitHubRepository', function () {
2930
it('detects when the remote is pointing to github.com', function () {
3031
const url = 'https://github.com/some/repo';
3132
const remote = new Remote('origin', url, new Protocol(url));
32-
const dotcomRepository = new GitHubRepository(remote, credentialStore, telemetry, new MockSessionState());
33+
const rootUri = Uri.file('C:\\users\\test\\repo');
34+
const dotcomRepository = new GitHubRepository(remote, rootUri, credentialStore, telemetry, new MockSessionState());
3335
assert(dotcomRepository.isGitHubDotCom);
3436
});
3537

3638
it('detects when the remote is pointing somewhere other than github.com', function () {
3739
const url = 'https://github.enterprise.horse/some/repo';
3840
const remote = new Remote('origin', url, new Protocol(url));
39-
const dotcomRepository = new GitHubRepository(remote, credentialStore, telemetry, new MockSessionState());
41+
const rootUri = Uri.file('C:\\users\\test\\repo');
42+
const dotcomRepository = new GitHubRepository(remote, rootUri, credentialStore, telemetry, new MockSessionState());
4043
assert(!dotcomRepository.isGitHubDotCom);
4144
});
4245
});

src/test/mocks/mockGitHubRepository.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ import {
1414
} from '../builders/managedPullRequestBuilder';
1515
import { MockTelemetry } from './mockTelemetry';
1616
import { MockSessionState } from './mockSessionState';
17+
import { Uri } from 'vscode';
1718
const queries = require('../../github/queries.gql');
1819

1920
export class MockGitHubRepository extends GitHubRepository {
2021
readonly queryProvider: QueryProvider;
2122

2223
constructor(remote: Remote, credentialStore: CredentialStore, telemetry: MockTelemetry, sinon: SinonSandbox) {
23-
super(remote, credentialStore, telemetry, new MockSessionState());
24+
super(remote, Uri.file('C:\\users\\test\\repo'),credentialStore, telemetry, new MockSessionState());
2425

2526
this.queryProvider = new QueryProvider(sinon);
2627

src/view/reviewManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ export class ReviewManager {
523523
const contentChanges = await pr.getFileChangesInfo(this._repository);
524524
this._reviewModel.localFileChanges = await this.getLocalChangeNodes(pr, contentChanges);
525525
await Promise.all([pr.initializeReviewComments(), pr.initializeReviewThreadCache(), pr.initializePullRequestFileViewState()]);
526+
pr.setFileViewedContext();
526527
const outdatedComments = pr.comments.filter(comment => !comment.position);
527528

528529
const commitsGroup = groupBy(outdatedComments, comment => comment.originalCommitId!);

0 commit comments

Comments
 (0)