Add a button to open in android studio.

This commit is contained in:
Greyson Parrelli committed 2026-09-28 16:38:51 -04:00
1 parent 0e8bc70151
commit bb90255002
8 files changed
+298

No files matched your search

+11
View File
@@ -629,6 +629,16 @@ export default function App() {
setLocate({ file, seq: ++locateSeq.current });
}, []);
// Success is Studio coming to the front, so only a failure needs saying here.
const openInStudio = useCallback(
(file: string, line?: number) => {
api
.openInStudio(file, line)
.catch((e) => flash(e instanceof Error ? e.message : String(e)));
},
[flash],
);
const draftCount = useMemo(
() => comments.filter((c) => c.status === 'draft').length,
[comments],
@@ -963,6 +973,7 @@ export default function App() {
reveal={reveal}
onSetViewed={setFileViewed}
onLocate={locateFile}
onOpenInStudio={repo?.studio ? openInStudio : undefined}
onStartDraft={setDraft}
onCancelDraft={cancelDraft}
onSubmitDraft={submitDraft}
+5
View File
@@ -202,6 +202,11 @@ export const api = {
reopen: (id: string) => postJSON(`${apiBase}/comments/${id}/reopen`).then(json<Comment>),
// Submits every draft in the review — the same set the rail shows.
// Hands the file to Android Studio's launcher, which opens it in the window of
// the project containing it. Resolves once the launcher has started, not once
// the file is on screen: nothing comes back from the IDE either way.
openInStudio: (path: string, line?: number) =>
postJSON(`${apiBase}/studio/open`, { path, line }).then(json<void>),
submit: () => postJSON(`${apiBase}/review/submit`).then(json<{ submitted: number }>),
// Deletes every comment, whatever its status. The reviewer's viewed marks are
+37
View File
@@ -66,6 +66,9 @@ interface Props {
onSetViewed: (file: string, viewed: boolean, source?: string) => void;
// Reveals the file in the left rail's file list.
onLocate: (file: string) => void;
// Opens the file in Android Studio at `line`. Absent when there's no Studio to
// open it in, which hides the button.
onOpenInStudio?: (file: string, line?: number) => void;
onStartDraft: (d: DraftTarget) => void;
onCancelDraft: () => void;
onSubmitDraft: (body: string) => Promise<void>;
@@ -435,6 +438,7 @@ export function DiffView({
reveal,
onSetViewed,
onLocate,
onOpenInStudio,
onStartDraft,
onCancelDraft,
onSubmitDraft,
@@ -484,6 +488,7 @@ export function DiffView({
bodyRef={bodyRef(path)}
onSetViewed={onSetViewed}
onLocate={onLocate}
onOpenInStudio={onOpenInStudio}
onStartDraft={onStartDraft}
onCancelDraft={onCancelDraft}
onSubmitDraft={onSubmitDraft}
@@ -495,6 +500,21 @@ export function DiffView({
);
}
// Where a file's first change sits in the work tree — the line to open it at. A
// deletion has no line of its own on the new side, so it lands on the line that
// now sits where the removed ones were.
function firstChangedLine(hunks: HunkData[]): number | undefined {
const hunk = hunks[0];
if (!hunk) return undefined;
let next = hunk.newStart;
for (const change of hunk.changes) {
if (change.type === 'insert') return change.lineNumber;
if (change.type === 'delete') return Math.max(1, next);
next = change.newLineNumber + 1;
}
return Math.max(1, hunk.newStart);
}
function statusLabel(type: string): string {
switch (type) {
case 'add':
@@ -542,6 +562,7 @@ const FileView = memo(function FileView({
bodyRef,
onSetViewed,
onLocate,
onOpenInStudio,
onStartDraft,
onCancelDraft,
onSubmitDraft,
@@ -567,6 +588,7 @@ const FileView = memo(function FileView({
bodyRef: (el: HTMLElement | null) => void;
onSetViewed: Props['onSetViewed'];
onLocate: Props['onLocate'];
onOpenInStudio: Props['onOpenInStudio'];
onStartDraft: (d: DraftTarget) => void;
onCancelDraft: () => void;
onSubmitDraft: Props['onSubmitDraft'];
@@ -763,6 +785,21 @@ const FileView = memo(function FileView({
>
<Icon name="focus-center" />
</button>
{onOpenInStudio && file.type !== 'delete' && (
<button
className="icon-btn"
onClick={() =>
onOpenInStudio(
path,
firstChangedLine(showingSince ? sinceHunks! : file.hunks),
)
}
title="Open in Android Studio"
aria-label="Open in Android Studio"
>
<Icon name="code" />
</button>
)}
{file.type !== 'modify' && (
<span className={`file-status file-status-${file.type}`}>
{statusLabel(file.type)}
+1
View File
@@ -21,6 +21,7 @@ const PATHS = {
'M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z',
'file-diff':
'M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z',
code: 'm11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z',
copy: 'M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25ZM5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z',
unfold:
'm8.177.677 2.896 2.896a.25.25 0 0 1-.177.427H8.75v1.25a.75.75 0 0 1-1.5 0V4H5.104a.25.25 0 0 1-.177-.427L7.823.677a.25.25 0 0 1 .354 0ZM7.25 10.75a.75.75 0 0 1 1.5 0V12h2.146a.25.25 0 0 1 .177.427l-2.896 2.896a.25.25 0 0 1-.354 0l-2.896-2.896A.25.25 0 0 1 5.104 12H7.25v-1.25Zm-5-2a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM6 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 6 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5ZM12 8a.75.75 0 0 1-.75.75h-.5a.75.75 0 0 1 0-1.5h.5A.75.75 0 0 1 12 8Zm2.25.75a.75.75 0 0 0 0-1.5h-.5a.75.75 0 0 0 0 1.5h.5Z',
+3
View File
@@ -96,6 +96,9 @@ export interface RepoState extends RepoInfo {
// page publishes one. The browser is the source of truth for what's on screen;
// this copy is what lets an agent review, and anchor comments to, the same diff.
context?: DiffContext | null;
// Whether this machine has an Android Studio for the file headers to open
// files in (see api.openInStudio).
studio: boolean;
}
export interface DiffFile {