Skip to content

Commit b07ad9d

Browse files
Persist selection and file in the url for loading later (codesandbox#3841)
* Persist selection and file in the url for loading later * Fix typecheck * Handle selection in editor * Fix where we persist the cursor state * Update packages/app/src/app/overmind/namespaces/editor/actions.ts Co-Authored-By: Michaël De Boey <[email protected]> * Update packages/app/src/app/overmind/namespaces/live/actions.ts Co-Authored-By: Michaël De Boey <[email protected]> * Update packages/app/src/app/pages/Sandbox/index.tsx Co-Authored-By: Michaël De Boey <[email protected]> * Update packages/app/src/app/overmind/namespaces/editor/actions.ts Co-Authored-By: Michaël De Boey <[email protected]> * Update packages/app/src/app/overmind/effects/vscode/index.ts Co-Authored-By: Michaël De Boey <[email protected]> * Move try catch statement * Change encoding of the URL * Change encoding of url Co-authored-by: Michaël De Boey <[email protected]>
1 parent f5c0ceb commit b07ad9d

File tree

7 files changed

+290
-148
lines changed

7 files changed

+290
-148
lines changed

packages/app/src/app/overmind/effects/router.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getSandboxOptions } from '@codesandbox/common/lib/url';
33
import { sandboxUrl } from '@codesandbox/common/lib/utils/url-generator';
44
import history from '../../utils/history';
55

6-
export default {
6+
export default new (class RouterEffect {
77
replaceSandboxUrl({
88
id,
99
alias,
@@ -14,7 +14,8 @@ export default {
1414
git?: GitInfo | null;
1515
}) {
1616
window.history.replaceState({}, '', sandboxUrl({ id, alias, git }));
17-
},
17+
}
18+
1819
updateSandboxUrl(
1920
{
2021
id,
@@ -38,20 +39,35 @@ export default {
3839
} else {
3940
history.push(url);
4041
}
41-
},
42+
}
43+
4244
redirectToNewSandbox() {
4345
history.push('/s/new');
44-
},
46+
}
47+
4548
redirectToSandboxWizard() {
4649
history.replace('/s/');
47-
},
50+
}
51+
4852
getSandboxOptions() {
4953
return getSandboxOptions(decodeURIComponent(document.location.href));
50-
},
54+
}
55+
5156
getCommentId() {
52-
return new URL(location.href).searchParams.get('comment');
53-
},
57+
return this.getParameter('comment');
58+
}
59+
5460
createCommentUrl(id: string) {
5561
return `${window.location.origin}${window.location.pathname}?comment=${id}`;
56-
},
57-
};
62+
}
63+
64+
replace(url: string) {
65+
const origin = new URL(url).origin;
66+
history.replace(url.replace(origin, ''));
67+
}
68+
69+
getParameter(key: string): string | null {
70+
const currentUrl = new URL(location.href);
71+
return currentUrl.searchParams.get(key);
72+
}
73+
})();

packages/app/src/app/overmind/effects/vscode/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,41 @@ export class VSCodeEffect {
655655
}
656656
}
657657

658+
/**
659+
* Set the selection inside the editor
660+
* @param head Start of the selection
661+
* @param anchor End of the selection
662+
*/
663+
setSelection(head: number, anchor: number) {
664+
const activeEditor = this.editorApi.getActiveCodeEditor();
665+
if (!activeEditor) {
666+
return;
667+
}
668+
669+
const model = activeEditor.getModel();
670+
if (!model) {
671+
return;
672+
}
673+
674+
const headPos = indexToLineAndColumn(
675+
model.getLinesContent() || [],
676+
head
677+
);
678+
const anchorPos = indexToLineAndColumn(
679+
model.getLinesContent() || [],
680+
anchor
681+
);
682+
const range = new this.monaco.Range(
683+
headPos.lineNumber,
684+
headPos.column,
685+
anchorPos.lineNumber,
686+
anchorPos.column
687+
);
688+
689+
this.revealRange(range);
690+
activeEditor.setSelection(range);
691+
}
692+
658693
// Communicates the endpoint for the WebsocketLSP
659694
private createContainerForkHandler() {
660695
return () => {

packages/app/src/app/overmind/namespaces/editor/actions.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { debounce } from 'lodash-es';
12
import { resolveModule } from '@codesandbox/common/lib/sandbox/modules';
23
import getTemplate from '@codesandbox/common/lib/templates';
34
import {
@@ -6,6 +7,8 @@ import {
67
ModuleError,
78
ModuleTab,
89
WindowOrientation,
10+
Module,
11+
UserSelection,
912
} from '@codesandbox/common/lib/types';
1013
import { logBreadcrumb } from '@codesandbox/common/lib/utils/analytics/sentry';
1114
import { getTextOperation } from '@codesandbox/common/lib/utils/diff';
@@ -41,6 +44,59 @@ export const internal = internalActions;
4144

4245
export const onNavigateAway: Action = () => {};
4346

47+
export const persistCursorToUrl: Action<{
48+
module: Module;
49+
selection?: UserSelection;
50+
}> = debounce(({ effects }, { module, selection }) => {
51+
let parameter = module.path;
52+
53+
if (selection?.primary?.selection?.length) {
54+
const [head, anchor] = selection.primary.selection;
55+
const serializedSelection = head + '-' + anchor;
56+
parameter += `:${serializedSelection}`;
57+
}
58+
59+
const newUrl = new URL(document.location.href);
60+
newUrl.searchParams.set('file', parameter);
61+
62+
// Restore the URI encoded parts to their original values. Our server handles this well
63+
// and all the browsers do too.
64+
effects.router.replace(
65+
newUrl
66+
.toString()
67+
.replace(/%2F/g, '/')
68+
.replace('%3A', ':')
69+
);
70+
}, 500);
71+
72+
export const loadCursorFromUrl: AsyncAction = async ({
73+
effects,
74+
actions,
75+
state,
76+
}) => {
77+
if (!state.editor.currentSandbox) {
78+
return;
79+
}
80+
81+
const parameter = effects.router.getParameter('file');
82+
if (!parameter) {
83+
return;
84+
}
85+
const [path, selection] = parameter.split(':');
86+
87+
const module = state.editor.currentSandbox.modules.find(m => m.path === path);
88+
if (!module) {
89+
return;
90+
}
91+
92+
await actions.editor.moduleSelected({ id: module.id });
93+
94+
const [parsedHead, parsedAnchor] = selection.split('-').map(Number);
95+
if (!isNaN(parsedHead) && !isNaN(parsedAnchor)) {
96+
effects.vscode.setSelection(parsedHead, parsedAnchor);
97+
}
98+
};
99+
44100
export const addNpmDependency: AsyncAction<{
45101
name: string;
46102
version?: string;
@@ -217,6 +273,14 @@ export const sandboxChanged: AsyncAction<{ id: string }> = withLoadApp<{
217273
}
218274

219275
effects.vscode.openModule(state.editor.currentModule);
276+
try {
277+
await actions.editor.loadCursorFromUrl();
278+
} catch (e) {
279+
/**
280+
* This is not extremely important logic, if it breaks (which is possible because of user input)
281+
* we don't want to crash the whole editor. That's why we try...catch this.
282+
*/
283+
}
220284

221285
if (COMMENTS && hasPermission(sandbox.authorization, 'comment')) {
222286
actions.comments.getSandboxComments(sandbox.id);
@@ -512,12 +576,17 @@ export const moduleSelected: AsyncAction<
512576
)
513577
: sandbox.modules.filter(moduleItem => moduleItem.id === id)[0];
514578

515-
if (module.shortid === state.editor.currentModuleShortid) {
579+
if (module.shortid === state.editor.currentModuleShortid && path) {
580+
// If this comes from VSCode we can return, but if this call comes from CodeSandbox
581+
// we shouldn't return, since the promise would resolve sooner than VSCode loaded
582+
// the file
516583
return;
517584
}
518585

519586
await actions.editor.internal.setCurrentModule(module);
520587

588+
actions.editor.persistCursorToUrl({ module });
589+
521590
if (state.live.isLive && state.live.liveUser && state.live.roomInfo) {
522591
actions.editor.internal.updateSelectionsOfModule({ module });
523592
state.live.liveUser.currentModuleShortid = module.shortid;
@@ -781,6 +850,20 @@ export const showEnvironmentVariablesNotification: AsyncAction = async ({
781850
}
782851
};
783852

853+
export const onSelectionChanged: Action<UserSelection> = (
854+
{ actions, state },
855+
selection
856+
) => {
857+
if (!state.editor.currentModule) {
858+
return;
859+
}
860+
861+
actions.editor.persistCursorToUrl({
862+
module: state.editor.currentModule,
863+
selection,
864+
});
865+
};
866+
784867
export const toggleEditorPreviewLayout: Action = ({ state, effects }) => {
785868
const currentOrientation = state.editor.previewWindowOrientation;
786869

packages/app/src/app/overmind/namespaces/live/actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ export const revealViewRange: Action<{ liveUserId: string }> = (
422422
}
423423
};
424424

425-
export const revealCursorPosition: Action<{ liveUserId: string }> = (
425+
export const revealCursorPosition: AsyncAction<{ liveUserId: string }> = async (
426426
{ state, effects, actions },
427427
{ liveUserId }
428428
) => {
@@ -438,7 +438,7 @@ export const revealCursorPosition: Action<{ liveUserId: string }> = (
438438
({ shortid }) => shortid === user.currentModuleShortid
439439
)[0];
440440

441-
actions.editor.moduleSelected({ id: module.id });
441+
await actions.editor.moduleSelected({ id: module.id });
442442

443443
if (user.selection?.primary?.cursorPosition) {
444444
effects.vscode.revealPositionInCenterIfOutsideViewport(

packages/app/src/app/overmind/onInitialize.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ export const onInitialize: OnInitialize = async (
8383
getCurrentUser: () => state.user,
8484
onOperationApplied: actions.editor.onOperationApplied,
8585
onCodeChange: actions.editor.codeChanged,
86-
onSelectionChanged: actions.live.onSelectionChanged,
86+
onSelectionChanged: selection => {
87+
actions.editor.onSelectionChanged(selection);
88+
actions.live.onSelectionChanged(selection);
89+
},
8790
onViewRangeChanged: actions.live.onViewRangeChanged,
8891
onCommentClick: actions.comments.onCommentClick,
8992
reaction: overmindInstance.reaction,

packages/app/src/app/pages/Sandbox/Editor/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const StatusBar = styled.div`
2929
}
3030
`;
3131

32-
const ContentSplit = () => {
32+
const Editor = () => {
3333
const { state, actions, effects, reaction } = useOvermind();
3434
const statusbarEl = useRef(null);
3535
const [showSkeleton, setShowSkeleton] = useState(
@@ -211,4 +211,4 @@ const ContentSplit = () => {
211211
);
212212
};
213213

214-
export default ContentSplit;
214+
export default Editor;

0 commit comments

Comments
 (0)