Loading src/desktop/review/change_type_decoration_provider.test.ts +20 −2 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { changeTypeDecorationProvider, decorations } from './change_type_decoration_provider'; import { ADDED, DELETED, RENAMED, MODIFIED, CHANGE_TYPE_QUERY_KEY } from '../constants'; import { toReviewUri } from './review_uri'; describe('FileDecoratorProvider', () => { it.each` const commonParams = { path: './test', exists: true, mrId: 1, projectId: 1, repositoryRoot: '/', }; describe.each` changeType | decoration ${ADDED} | ${decorations[ADDED]} ${DELETED} | ${decorations[DELETED]} ${RENAMED} | ${decorations[RENAMED]} ${MODIFIED} | ${decorations[MODIFIED]} `('Correctly maps changeType to decorator', ({ changeType, decoration }) => { `('Change type $changeType', ({ changeType, decoration }) => { it('decorates File urls', () => { const uri: vscode.Uri = vscode.Uri.file(`./test?${CHANGE_TYPE_QUERY_KEY}=${changeType}`); const { token } = new vscode.CancellationTokenSource(); const returnValue = changeTypeDecorationProvider.provideFileDecoration(uri, token); expect(returnValue).toEqual(decoration); }); it('decorates Review urls', () => { const uri: vscode.Uri = toReviewUri({ ...commonParams, change: changeType }); const { token } = new vscode.CancellationTokenSource(); const returnValue = changeTypeDecorationProvider.provideFileDecoration(uri, token); expect(returnValue).toEqual(decoration); }); }); }); src/desktop/review/change_type_decoration_provider.ts +17 −1 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { ADDED, DELETED, RENAMED, MODIFIED, CHANGE_TYPE_QUERY_KEY } from '../constants'; import { ADDED, DELETED, RENAMED, MODIFIED, CHANGE_TYPE_QUERY_KEY, REVIEW_URI_SCHEME, } from '../constants'; import { fromReviewUri } from './review_uri'; export const decorations: Record<string, vscode.FileDecoration | undefined> = { [ADDED]: { Loading @@ -21,6 +29,14 @@ export const decorations: Record<string, vscode.FileDecoration | undefined> = { export const changeTypeDecorationProvider: vscode.FileDecorationProvider = { provideFileDecoration: uri => { if (uri.scheme === REVIEW_URI_SCHEME) { const params = fromReviewUri(uri); const changeType = params.change; if (changeType) { return decorations[changeType]; } return decorations[DELETED]; } if (uri.scheme === 'file') { const params = new URLSearchParams(uri.query); const changeType = params.get(CHANGE_TYPE_QUERY_KEY); Loading src/desktop/review/review_uri.ts +5 −2 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import { jsonStringifyWithSortedKeys } from '../utils/json_stringify_with_sorted export interface ReviewParams { path: string; exists: boolean; change?: string; commit?: string; // if the review URI only contains the mandatory params, we treat it as an empty file URI (that's used when showing diff of added/deleted file) repositoryRoot: string; Loading @@ -15,12 +16,13 @@ export interface ReviewParams { export function toReviewUri({ path, exists, change, commit, repositoryRoot, projectId, mrId, }: ReviewParams): Uri { const query = { commit, exists: exists ? '1' : '', repositoryRoot, projectId, mrId }; const query = { commit, change, exists: exists ? '1' : '', repositoryRoot, projectId, mrId }; return Uri.file(path).with({ scheme: REVIEW_URI_SCHEME, query: jsonStringifyWithSortedKeys(query), Loading @@ -28,11 +30,12 @@ export function toReviewUri({ } export function fromReviewUri(uri: Uri): ReviewParams { const { commit, exists, repositoryRoot, projectId, mrId } = JSON.parse(uri.query); const { commit, change, exists, repositoryRoot, projectId, mrId } = JSON.parse(uri.query); return { path: uri.path, exists: !!exists, commit, change, repositoryRoot, projectId, mrId, Loading src/desktop/tree_view/items/changed_file_item.test.ts +46 −0 Original line number Diff line number Diff line import { VS_COMMANDS } from '../../command_names'; import { ADDED, CHANGE_TYPE_QUERY_KEY, Loading @@ -12,6 +13,49 @@ import { ChangedFileItem } from './changed_file_item'; describe('ChangedFileItem', () => { describe('image file', () => { const extensions = ['.jpg', '.png', '.mp3', '.webm']; it.each(extensions)('should show diff for changed %s file', extension => { const changedImageFile = { ...diffFile, new_path: `file${extension}` }; const item = new ChangedFileItem(mr, mrVersion, changedImageFile, '/repo', () => false); expect(item.command?.command).toBe(VS_COMMANDS.DIFF); }); it.each(extensions)('should not show diff for new %s file', extension => { const path = `newfile${extension}`; const changedImageFile = { ...diffFile, renamed_file: false, new_file: true, old_path: `invalid${extension}`, new_path: path, }; const item = new ChangedFileItem(mr, mrVersion, changedImageFile, '/repo', () => false); expect(item.command?.command).toBe(VS_COMMANDS.OPEN); const [url] = item.command?.arguments ?? []; expect(url.path).toBe(path); }); it.each(extensions)('should not show diff for deleted %s file', extension => { const path = `deleted${extension}`; const changedImageFile = { ...diffFile, renamed_file: false, deleted_file: true, old_path: path, new_path: `invalid${extension}`, }; const item = new ChangedFileItem(mr, mrVersion, changedImageFile, '/repo', () => false); expect(item.command?.command).toBe(VS_COMMANDS.OPEN); const [url] = item.command?.arguments ?? []; expect(url.path).toBe(path); }); it.each` file | changeType ${{ ...diffFile, new_file: true, deleted_file: false, renamed_file: false }} | ${ADDED} Loading Loading @@ -49,6 +93,7 @@ describe('ChangedFileItem', () => { toReviewUri({ path: diffFile.old_path, exists: true, change: 'renamed', commit: mrVersion.base_commit_sha, repositoryRoot: '/repository/fsPath', projectId: mr.project_id, Loading @@ -59,6 +104,7 @@ describe('ChangedFileItem', () => { toReviewUri({ path: diffFile.new_path, exists: true, change: 'renamed', commit: mrVersion.head_commit_sha, repositoryRoot: '/repository/fsPath', projectId: mr.project_id, Loading src/desktop/tree_view/items/changed_file_item.ts +35 −1 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { posix as path } from 'path'; import { toReviewUri } from '../../review/review_uri'; import { isEmptyFileUri, toReviewUri } from '../../review/review_uri'; import { VS_COMMANDS } from '../../command_names'; import { ADDED, Loading @@ -21,6 +21,28 @@ const getChangeType = (file: RestDiffFile): ChangeType => { return MODIFIED; }; // All extensions supported by VS Code's built-in Media Previewer. // https://github.com/microsoft/vscode/blob/ba38f8a5eeb81c05dbdade3e1657e233d770f717/extensions/media-preview/package.json const mediaExtensions = [ '.jpg', '.jpe', '.jpeg', '.png', '.bmp', '.gif', '.ico', '.webp', '.avif', '.mp3', '.wav', '.ogg', '.oga', '.mp4', '.webm', ]; const looksLikeMedia = (filePath: string) => mediaExtensions.includes(path.extname(filePath).toLowerCase()); const getBaseAndHeadUri = ( mr: RestMr, mrVersion: RestMrVersion, Loading @@ -29,6 +51,7 @@ const getBaseAndHeadUri = ( ) => { const commonParams = { repositoryRoot: repositoryPath, change: getChangeType(file), projectId: mr.project_id, mrId: mr.id, }; Loading Loading @@ -69,12 +92,22 @@ export class ChangedFileItem extends vscode.TreeItem { const uris = getBaseAndHeadUri(mr, mrVersion, file, repositoryPath); this.headFileUri = uris.headFileUri; this.baseFileUri = uris.baseFileUri; const headIsEmpty = isEmptyFileUri(uris.headFileUri); const baseIsEmpty = isEmptyFileUri(uris.baseFileUri); const hasComments = hasComment(this.baseFileUri) || hasComment(this.headFileUri); const query = new URLSearchParams([ [CHANGE_TYPE_QUERY_KEY, getChangeType(file)], [HAS_COMMENTS_QUERY_KEY, String(hasComments)], ]).toString(); this.resourceUri = this.resourceUri?.with({ query }); if (looksLikeMedia(uris.headFileUri.path) && (headIsEmpty || baseIsEmpty)) { this.command = { title: 'Show changes', command: VS_COMMANDS.OPEN, arguments: [headIsEmpty ? this.baseFileUri : this.headFileUri], }; } else { this.command = { title: 'Show changes', command: VS_COMMANDS.DIFF, Loading @@ -86,3 +119,4 @@ export class ChangedFileItem extends vscode.TreeItem { }; } } } Loading
src/desktop/review/change_type_decoration_provider.test.ts +20 −2 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { changeTypeDecorationProvider, decorations } from './change_type_decoration_provider'; import { ADDED, DELETED, RENAMED, MODIFIED, CHANGE_TYPE_QUERY_KEY } from '../constants'; import { toReviewUri } from './review_uri'; describe('FileDecoratorProvider', () => { it.each` const commonParams = { path: './test', exists: true, mrId: 1, projectId: 1, repositoryRoot: '/', }; describe.each` changeType | decoration ${ADDED} | ${decorations[ADDED]} ${DELETED} | ${decorations[DELETED]} ${RENAMED} | ${decorations[RENAMED]} ${MODIFIED} | ${decorations[MODIFIED]} `('Correctly maps changeType to decorator', ({ changeType, decoration }) => { `('Change type $changeType', ({ changeType, decoration }) => { it('decorates File urls', () => { const uri: vscode.Uri = vscode.Uri.file(`./test?${CHANGE_TYPE_QUERY_KEY}=${changeType}`); const { token } = new vscode.CancellationTokenSource(); const returnValue = changeTypeDecorationProvider.provideFileDecoration(uri, token); expect(returnValue).toEqual(decoration); }); it('decorates Review urls', () => { const uri: vscode.Uri = toReviewUri({ ...commonParams, change: changeType }); const { token } = new vscode.CancellationTokenSource(); const returnValue = changeTypeDecorationProvider.provideFileDecoration(uri, token); expect(returnValue).toEqual(decoration); }); }); });
src/desktop/review/change_type_decoration_provider.ts +17 −1 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { ADDED, DELETED, RENAMED, MODIFIED, CHANGE_TYPE_QUERY_KEY } from '../constants'; import { ADDED, DELETED, RENAMED, MODIFIED, CHANGE_TYPE_QUERY_KEY, REVIEW_URI_SCHEME, } from '../constants'; import { fromReviewUri } from './review_uri'; export const decorations: Record<string, vscode.FileDecoration | undefined> = { [ADDED]: { Loading @@ -21,6 +29,14 @@ export const decorations: Record<string, vscode.FileDecoration | undefined> = { export const changeTypeDecorationProvider: vscode.FileDecorationProvider = { provideFileDecoration: uri => { if (uri.scheme === REVIEW_URI_SCHEME) { const params = fromReviewUri(uri); const changeType = params.change; if (changeType) { return decorations[changeType]; } return decorations[DELETED]; } if (uri.scheme === 'file') { const params = new URLSearchParams(uri.query); const changeType = params.get(CHANGE_TYPE_QUERY_KEY); Loading
src/desktop/review/review_uri.ts +5 −2 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import { jsonStringifyWithSortedKeys } from '../utils/json_stringify_with_sorted export interface ReviewParams { path: string; exists: boolean; change?: string; commit?: string; // if the review URI only contains the mandatory params, we treat it as an empty file URI (that's used when showing diff of added/deleted file) repositoryRoot: string; Loading @@ -15,12 +16,13 @@ export interface ReviewParams { export function toReviewUri({ path, exists, change, commit, repositoryRoot, projectId, mrId, }: ReviewParams): Uri { const query = { commit, exists: exists ? '1' : '', repositoryRoot, projectId, mrId }; const query = { commit, change, exists: exists ? '1' : '', repositoryRoot, projectId, mrId }; return Uri.file(path).with({ scheme: REVIEW_URI_SCHEME, query: jsonStringifyWithSortedKeys(query), Loading @@ -28,11 +30,12 @@ export function toReviewUri({ } export function fromReviewUri(uri: Uri): ReviewParams { const { commit, exists, repositoryRoot, projectId, mrId } = JSON.parse(uri.query); const { commit, change, exists, repositoryRoot, projectId, mrId } = JSON.parse(uri.query); return { path: uri.path, exists: !!exists, commit, change, repositoryRoot, projectId, mrId, Loading
src/desktop/tree_view/items/changed_file_item.test.ts +46 −0 Original line number Diff line number Diff line import { VS_COMMANDS } from '../../command_names'; import { ADDED, CHANGE_TYPE_QUERY_KEY, Loading @@ -12,6 +13,49 @@ import { ChangedFileItem } from './changed_file_item'; describe('ChangedFileItem', () => { describe('image file', () => { const extensions = ['.jpg', '.png', '.mp3', '.webm']; it.each(extensions)('should show diff for changed %s file', extension => { const changedImageFile = { ...diffFile, new_path: `file${extension}` }; const item = new ChangedFileItem(mr, mrVersion, changedImageFile, '/repo', () => false); expect(item.command?.command).toBe(VS_COMMANDS.DIFF); }); it.each(extensions)('should not show diff for new %s file', extension => { const path = `newfile${extension}`; const changedImageFile = { ...diffFile, renamed_file: false, new_file: true, old_path: `invalid${extension}`, new_path: path, }; const item = new ChangedFileItem(mr, mrVersion, changedImageFile, '/repo', () => false); expect(item.command?.command).toBe(VS_COMMANDS.OPEN); const [url] = item.command?.arguments ?? []; expect(url.path).toBe(path); }); it.each(extensions)('should not show diff for deleted %s file', extension => { const path = `deleted${extension}`; const changedImageFile = { ...diffFile, renamed_file: false, deleted_file: true, old_path: path, new_path: `invalid${extension}`, }; const item = new ChangedFileItem(mr, mrVersion, changedImageFile, '/repo', () => false); expect(item.command?.command).toBe(VS_COMMANDS.OPEN); const [url] = item.command?.arguments ?? []; expect(url.path).toBe(path); }); it.each` file | changeType ${{ ...diffFile, new_file: true, deleted_file: false, renamed_file: false }} | ${ADDED} Loading Loading @@ -49,6 +93,7 @@ describe('ChangedFileItem', () => { toReviewUri({ path: diffFile.old_path, exists: true, change: 'renamed', commit: mrVersion.base_commit_sha, repositoryRoot: '/repository/fsPath', projectId: mr.project_id, Loading @@ -59,6 +104,7 @@ describe('ChangedFileItem', () => { toReviewUri({ path: diffFile.new_path, exists: true, change: 'renamed', commit: mrVersion.head_commit_sha, repositoryRoot: '/repository/fsPath', projectId: mr.project_id, Loading
src/desktop/tree_view/items/changed_file_item.ts +35 −1 Original line number Diff line number Diff line import * as vscode from 'vscode'; import { posix as path } from 'path'; import { toReviewUri } from '../../review/review_uri'; import { isEmptyFileUri, toReviewUri } from '../../review/review_uri'; import { VS_COMMANDS } from '../../command_names'; import { ADDED, Loading @@ -21,6 +21,28 @@ const getChangeType = (file: RestDiffFile): ChangeType => { return MODIFIED; }; // All extensions supported by VS Code's built-in Media Previewer. // https://github.com/microsoft/vscode/blob/ba38f8a5eeb81c05dbdade3e1657e233d770f717/extensions/media-preview/package.json const mediaExtensions = [ '.jpg', '.jpe', '.jpeg', '.png', '.bmp', '.gif', '.ico', '.webp', '.avif', '.mp3', '.wav', '.ogg', '.oga', '.mp4', '.webm', ]; const looksLikeMedia = (filePath: string) => mediaExtensions.includes(path.extname(filePath).toLowerCase()); const getBaseAndHeadUri = ( mr: RestMr, mrVersion: RestMrVersion, Loading @@ -29,6 +51,7 @@ const getBaseAndHeadUri = ( ) => { const commonParams = { repositoryRoot: repositoryPath, change: getChangeType(file), projectId: mr.project_id, mrId: mr.id, }; Loading Loading @@ -69,12 +92,22 @@ export class ChangedFileItem extends vscode.TreeItem { const uris = getBaseAndHeadUri(mr, mrVersion, file, repositoryPath); this.headFileUri = uris.headFileUri; this.baseFileUri = uris.baseFileUri; const headIsEmpty = isEmptyFileUri(uris.headFileUri); const baseIsEmpty = isEmptyFileUri(uris.baseFileUri); const hasComments = hasComment(this.baseFileUri) || hasComment(this.headFileUri); const query = new URLSearchParams([ [CHANGE_TYPE_QUERY_KEY, getChangeType(file)], [HAS_COMMENTS_QUERY_KEY, String(hasComments)], ]).toString(); this.resourceUri = this.resourceUri?.with({ query }); if (looksLikeMedia(uris.headFileUri.path) && (headIsEmpty || baseIsEmpty)) { this.command = { title: 'Show changes', command: VS_COMMANDS.OPEN, arguments: [headIsEmpty ? this.baseFileUri : this.headFileUri], }; } else { this.command = { title: 'Show changes', command: VS_COMMANDS.DIFF, Loading @@ -86,3 +119,4 @@ export class ChangedFileItem extends vscode.TreeItem { }; } } }