Repository navigation
Two tabs at once, side by side, with one sidebar - #492
Open
cerede2000 wants to merge 3 commits into
Open
cerede2000 wants to merge 3 commits into
cerede2000 wants to merge 3 commits into
Conversation
Off by default, and turned on in Settings → Preferences → Browse in tabs. A tab is an address rather than a folder, so the strip holds anything the application can be at: a folder, a document, the text editor, a terminal, the trash, the settings. Each tab keeps what it was on — its listing, what was selected in it, the rename it was in the middle of, the place it was scrolled to. The clipboard is deliberately not per tab: copying in one and pasting in another is the point of having two. One rule opens anything behind: the platform modifier held with the gesture that opens the thing — the double click on a row, the single click on a favourite, a volume or a place on the home page, and Enter on whatever the keyboard is on. On a row that modifier with a single click still adds to the selection, which is worth more than a tab. The middle button says the same thing without a modifier, anywhere. What a tab holds stays alive while another is in front. A document is not opened again when you come back to it; the text editor comes back where you left it without reading the file again — what you typed and had not saved, the selection, and the line you were reading, put back before the editor is shown so the file does not appear at the top and jump. A folder tab comes back the same way, and is refreshed quietly behind what you were already looking at. A tab opened behind is got ready before you reach it: the folder listed, the document opened in its viewer, the text file read, the shell started. That is Settings → Preferences → Prepare tabs opened in the background, and it is capped at three prepared editors at once. While a tab is still working it shows a spinner where its icon goes, as a browser does — for a document, until it is really in the editor rather than until the panel exists. Back and Forward belong to the tab, not to the window: each keeps its own trail, so going back in one never takes you where another was looking, and the buttons grey out when that tab has nowhere to go. The rest of what the strip does: tabs are dragged along the row, duplicated, kept (pinned to the front as their icon alone, with no cross and immune to "close them all"), closed one at a time or all at once, and closed by double click for whoever turns that on. Files and folders dragged out of a listing can be dropped onto a tab, which moves them into the folder that tab is on — or copies them with the modifier held. The strip never scrolls: tabs share the room and stop at a number an administrator sets under Settings → Tabs. A terminal is a place now. What a terminal *is* came out of the drawer that showed it, so a page at an address of its own can show one too and there can be as many shells as there are tabs; the drawer opens per tab, and a new tab only on the gesture that asks for one. Closing a tab is the one gesture that destroys what a tab was holding, so a screen with something to lose can leave a question for its own tab. Asking it meant the application had to be able to ask at all: `window.confirm` answers by stopping the world, which a dialog inside the page cannot, so the question is the application's own dialog and everything from the guard to the cross on the tab waits for the answer. The browser's boxes are gone from the rest of the window with it — twenty-three of them, `confirm`, `prompt` and `alert`, replaced by that dialog and by the notifications this application already had. The browser journey comes with it, and with the bench it needs. Most of what tabs do is what a browser does to a page that is not on screen, and that is not a question a unit test can ask: a page unmounted the moment another tab comes forward, a menu clipped away by the strip's own overflow, a shell that dies when the layout holding it does. Each of those passed its unit suite and was broken in a browser. So `frontend/e2e/app` holds one journey through the whole application — set up, sign in, upload, share, delete and restore, read a version, edit, browse in tabs — run by a second Playwright project against the real server entry point serving the real build, in a throwaway install the journey also reads from disk to check what actually landed there. It runs under `npm run test:e2e` beside the specs already here, and takes about half a minute. Two readers in one folder, and the two things they were sharing that they should not have been. Where a reader is in a folder has always been remembered against the folder. With tabs that is no longer enough, because two tabs can be in one folder at once and be in different places in it — so a tab's place is kept against the tab as well. It was, and it was then read as a number: `tabPlace(key) || get(folder)`. A tab sitting at the top answers nought, nought is false, and the fallback is the folder's memory that every tab on it shares. Two tabs on one folder, the first scrolled to the bottom and left there, the second never moved from the top: the second came back down beside the first. The top is an answer, so the question is now whether this tab has a place here at all. The permission to be put back was shared the same way. It is granted for a folder when somebody walks back up into it — and consumed by whichever tab is drawn on that folder next, which need not be the tab that was promised it. One tab walking up out of a subfolder, or coming back out of a file, moved another reader. The permission names its tab now, and the rule that grants it moved out of the router's guard into `router/restorePermission.js` so that it can be asked directly, the way the other rules there are: it had no test, and what had no test is what went wrong. The Tabs page sits with the other preferences in the settings list rather than at the end of it.
Choose two or three text files in a listing, right-click, and Compare. They open at an address of their own, so a comparison lives in a tab, can be linked to, and is somewhere you leave and come back to rather than a dialog to finish. Every line of both files is there and aligned: two versions of a line sit on one row, and a gap shows where a side has nothing. Within a changed pair the part that actually differs is marked, which is what makes one character different in a long path readable. The alignment is written here rather than taken from a library because the answer has to be aligned and not merely listed: a patch says what changed, a comparison has to put line 40 of one file beside the line it corresponds to in the other. The differences are counted and numbered, and F8 and F7 walk them — alt with the arrows does the same, for a keyboard without those keys — coming round again at the end rather than stopping. Each difference can be taken across in either direction, a whole run at once, and the file written back keeps the line endings it had: a comparison that rewrote those would turn a one-line change into a change on every line. Three files align on the middle one, which is the arrangement that answers "who changed what" when two people have both edited a common version. Copies there go between neighbours, since left and right are not next to each other. Along the right: a bar the comparison draws for itself, because this platform draws its own over the content and fades it out when nothing is moving, and beside it a map of the differences — out of the way until the pointer nears the edge, pinned from the bar for whoever wants it there. The map is coloured the way the panes are and from the same single answer about what a line is: red where a line is only on the left, green where it is only on the right, amber where both have one and they differ. Search and replace looks in one file, another, or all of them at once, steps through what it finds, and replaces one or all — never into a side that is an earlier version. A line can be edited by hand: double-click it and type, with Enter breaking the line, backspace at the start joining it to the one above, the arrows walking from line to line, and a paste of several lines becoming several lines. The two sides can be put the other way round, address and tab name with them. A long file folds down to the differences with a little around them and says how much it folded, and that is always yours to unfold. In a tab, the tab is named after the files it compares, so three comparisons open are three tabs you can tell apart — and coming back to one reads neither file again: the lines, the difference you were on and anything taken across and not yet saved are all still there. A comparison with unsaved lines says so before you close its tab or the window. Also from the Versions panel: Compare with the file now puts an earlier version beside the file as it stands, the older one on the left, which is the order that reads as "what happened since". That side is read-only — there is nothing to write a version back to. Everything the text editor can open is comparable. COMPARE_FILE_EXTENSIONS adds whatever else an installation calls text, because that is a local question: somebody's .ino, somebody else's .tf.
A pair is a grouping of two tabs, not a split of the window, so there can be as many pairs as there are tabs and either half can hold anything a tab can hold: a folder, a document, a shell, a comparison. What belongs to a place is the pane's own; what belongs to a person stays one to the window. `files/folderTab.js` drew that line already — this is the rest of it. `TabPane.vue` is a pane. `paneTab.js` is how a screen on the far side asks which side it is on: with one pane it answers what `useRoute()` and "the tab in front" answered before, and with two it answers for the half the screen is drawn in. Every screen asks it now — the folder view, the breadcrumb, the selection, a document, an editor, a comparison, a shell — because each of them had been reading the window, and the window has only one answer. The window is the window. `WindowSidebar.vue` and `WindowBar.vue` are what `BrowserLayout` used to hold, drawn once above every layout so that a pane can hold anything: a document and a shell are outside that layout on purpose, and an editor and a comparison are in another one. `sidebar.js` is a module-level singleton because the toolbar above a pane opens the same sidebar the window draws. The routes for a document, an editor, a comparison and the trash are flat for the same reason: a layout that only passes its children through still breaks the symmetry between the halves, because the focused pane draws the outermost match and the other draws the deepest. `paneBoxes.js` measures where each pane is, for the two things drawn outside the page that have to sit on one anyway — a document in its viewer and a shell. Both are told which panes are on screen rather than which tab is in front, and both take their box from there. Four faults in the detail of it, each of them one sentence said a different way: A pane draws a tab, not an address. Bringing a tab forward is two moves — the store first, the address bar after — so the pane that has just taken the focus drew the router's screen for a tick while the router was still on the address of the half being left. A listing was replaced by a document and replaced back a frame later, which is why a press in the other half had to be made twice: the row it was pressed on no longer existed when the button came up. A pane now keeps its own screen until its tab and the address agree. A destroyed pane still names its tab. A page asking "am I still my tab?" on the way out is asking nothing in a pane that is being taken off screen, so the editor in the half beside the reader read "the address changed under me" and threw away where its reader was. It is the tab in front and the window's own address that say whether anyone navigated. A place in a folder is a row, not a number of pixels. A pane that comes back a different width — one of a pair, or a pair becoming one — re-flows its listing, and the same number is then a different place. And zero is an answer. A tab left at the top of a folder answers "nought", which read as "no answer", so the folder's own memory was taken instead — and that one is shared by every tab on the folder. Three tabs on one folder, one of them scrolled to the bottom to open a file, and the other two came back down beside it. Asked as "has this tab a place here" now, and the permission to be put back names its tab too.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This sits on #494, which sits on #493. Only the last commit is this one; review it
after those, or read
Two tabs at once, side by side, with one sidebaron its own.Two tabs drawn at once, side by side. Off unless you ask for it: right-click a tab →
Show on the right, and the two become one entry in the strip,
a ↔ b. Drag thedivider, Ungroup to undo. With no pair, every screen behaves exactly as it does
with tabs alone.
What a reader gets
A pair is two tabs, not a split of the window. So there can be as many pairs as
there are tabs, and either half holds anything a tab holds — a folder, a document in
its viewer, the text editor, a shell, a comparison. Copy on the left, paste on the
right. Drag a file from one half to the other. The two halves of one folder are two
readers of it: each keeps its own listing, its own selection and its own place in
it.
One sidebar, one address bar, for the window. They were inside the browser layout
and are now drawn once above every layout, which is what lets a pane hold a screen
that is not in that layout. The toolbar above a pane opens the same sidebar the
window draws.
How it is built, and why that way
What belongs to a place is the pane's own; what belongs to a person stays one to
the window.
stores/files/folderTab.jsdrew that line already — this is the rest ofit.
components/TabPane.vueis a pane.composables/paneTab.jsis how a screen on the far side of that line asks whichside it is on. With one pane it answers exactly what
useRoute()and "the tab infront" answered before; with two it answers for the half the screen is drawn in.
Every screen asks it now — the folder view, the breadcrumb, the selection, a
document, the editor, a comparison, a shell — because each of them was reading the
window, and the window has one answer.
composables/paneBoxes.jsmeasures where each pane is, for the two things drawnoutside the page that still have to sit on one: a document in its viewer and a
shell. Both are told which panes are on screen rather than which tab is in front.
layout that only passes its children through still breaks the symmetry between the
halves: the focused pane draws the outermost match and the other draws the deepest.
Four faults in the detail of it
Each is one sentence said a different way, and each has a test that fails without
the fix.
A pane draws a tab, not an address. Bringing a tab forward is two moves — the
store first, the address bar after — so the pane that had just taken the focus drew
the router's screen for one tick while the router was still on the address of the
half being left. A listing was replaced by a document and replaced back a frame
later, which is why a press in the other half had to be made twice: the row it was
pressed on no longer existed when the button came up. A pane keeps its own screen
until its tab and the address agree.
A destroyed pane still names its tab. A page asking "am I still my tab?" on the
way out is asking nothing in a pane being taken off screen — so the editor in the
half beside the reader read that as "the address changed under me" and threw away
where its reader was. Measured: the left half came back on its line, the right half
at the top of the file, every time. It is the tab in front and the window's own
address that say whether anybody navigated.
A place in a folder is a row, not a number of pixels. A pane that comes back a
different width — one of a pair, or a pair becoming one — re-flows its listing, and
the same number is then a different place.
A listing speaks only for an address it is the screen for. A pane handed a tab
that holds a file had, for one tick, a listing whose address was the file's. Reading
it asked the server to list a file as a folder:
ENOTDIR: not a directory, a 500,and nothing on screen to explain it.
Verified
npm run test:unitinfrontend/: 203 files, 3 522 tests, green onmainplus this stack (and the small FFmpeg catalogue fix, without which
main's owni18n suites fail — fifteen failures that are not this branch's).
scripts/check-batch.sh: merged ontomainin a fresh tree, the frontend bundles,the backend loads and migrates, and every client call has a route that answers it.
npm run test:e2e: the journey through the whole application, in a real browseragainst the real server serving the real build, walks a pair end to end — two tabs
on one folder each keeping their place and selection, a document opened from the
bottom of one half and shut again, crossing between two editors, a press in the
half beside a document taken on the first press.