AceNodeView: programmatic setSelection() marks the cursor dirty, scrolling the document to the front matter
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 1/5
- Tiempo estimado
- Menos de una hora
- Aptitud para principiantes
- 90/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- typescript
- Área
- frontend
Línea de trabajo
Comienza en packages/editor/src/optional/ace/ace.ts, en el método AceNodeView.setSelection y el handler changeCursor alrededor de las líneas referenciadas. Compara el handler con los guards de cambio de valor adyacentes y luego reproduce el comportamiento de foco/scroll usando el documento de Visual mode descrito; se considera terminado cuando restaurar el foco ya no desplaza el documento, mientras que el movimiento del cursor por parte del usuario sigue desplazando el cursor hasta hacerlo visible.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Bug description
AceNodeView treats a programmatic selection restore as a user cursor movement, which makes the visual editor scroll to whichever embedded Ace editor the ProseMirror selection happens to live in.
The changeCursor handler sets cursorDirty without consulting this.updating:
this.aceEditor.getSelection().on('changeCursor', () => {
if (this.dom.contains(document.activeElement) && !this.mouseDown) {
this.cursorDirty = true;
}
});
this.aceEditor.renderer.on('afterRender', () => {
if (this.cursorDirty && !this.mouseDown) {
this.scrollCursorIntoView();
this.cursorDirty = false;
}
});
But setSelection() — which ProseMirror calls from selectionToDOM() whenever EditorView.focus() runs — focuses the Ace editor and applies a selection range, and that fires changeCursor:
public setSelection(anchor: number, head: number) {
...
if (!this.escaping && !this.gapCursorPending) {
this.aceEditor.focus();
}
this.updating = true;
...
this.editSession.getSelection().setSelectionRange(range);
this.updating = false;
}
this.updating is set here for exactly this reason, and the value-change handlers a few lines above do check it (L508, L513) — the changeCursor handler is the one that doesn't. So the next Ace render calls scrollCursorIntoView(), which does container.scrollTop -= up on the editing root and yanks the whole document to that node view, even though nothing moved the cursor.
The visible damage depends on where the selection sits. It is worst for a freshly opened document: with no saved editing location the selection is at position 0, which is the YAML front matter block — itself an AceNodeView — so any EditorView.focus() scrolls the document to the very top.
We hit this in RStudio as rstudio/rstudio#18490: running a code chunk in a just-opened .qmd snaps the view to the top of the document and then scrolls back. RStudio calls EditorView.focus() as part of chunk execution (to make the editor the active view), which is enough to trigger it.
Steps to reproduce
In RStudio (any recent build, including current main):
- Create a
.qmdwith YAML front matter, a chunk, enough prose to make the document scroll, and a second chunk near the bottom:
---
title: "Untitled"
format: html
editor: visual
---
```{r setup, include=FALSE}
library(stats)
```
## Section one
<a dozen paragraphs of prose>
```{r}
x <- seq(-4, 4, length.out = 100)
head(x)
```
- Open it in Visual mode and do not click anywhere in the document.
- Scroll down and run the last chunk (toolbar button or Ctrl+Shift+Enter).
- Now click once anywhere in the prose and run the chunk again.
Generic form, independent of RStudio: put the ProseMirror selection inside an embedded code editor, scroll that editor out of view, and call EditorView.focus().
Actual behavior
Step 3 snaps the document to the top (to the front matter block's cursor) in a single frame, then smooth-scrolls back to the chunk. Measuring .pm-scroll-container scrollTop across a run:
run 1 before=413 min=74 -> back to 294
run 2 before=294 min=6 -> back to 294
Step 4 stops it: the selection is now an ordinary ProseMirror text selection rather than one inside an AceNodeView, so setSelection() is no longer routed to an Ace editor. It stays fixed until the document is closed and reopened (which discards the saved editing location and puts the selection back at position 0).
Captured stack at the scroll:
AceNodeView.scrollCursorIntoView
<- renderer 'afterRender' (cursorDirty)
<- Ace editor.focus() / setSelectionRange -> 'changeCursor'
<- AceNodeView.setSelection
<- NodeViewDesc.setSelection <- selectionToDOM <- EditorView.focus
Expected behavior
Restoring focus should not move the document. scrollCursorIntoView() should only run for cursor movement the user actually caused.
Suggested fix
Adding the same !this.updating guard the neighbouring handlers use is enough:
this.aceEditor.getSelection().on('changeCursor', () => {
- if (this.dom.contains(document.activeElement) && !this.mouseDown) {
+ if (!this.updating && this.dom.contains(document.activeElement) && !this.mouseDown) {
this.cursorDirty = true;
}
});
I A/B tested this by serving a patched panmirror.js to the browser while leaving everything else identical:
unpatched run 1 jump=215px run 2 jump=163px
patched run 1 no jump run 2 no jump
Legitimate scrollback is unaffected — scrolling a focused chunk out of view and pressing an arrow key still brings the cursor back into view in both builds.
Happy to send this as a PR if you'd like.
Your environment
- IDE: RStudio, development build of rstudio/rstudio
main; also reproduced by users on released 2026.07.1+147 - OS: macOS 26.6.2 (Apple Silicon); reporter of rstudio/rstudio#18490 on macOS 26.5.2
- panmirror sources checked against quarto-dev/quarto
main@ 828ae28e
- Lenguaje dominante
- TypeScript
- Estrellas
- 652
- Forks
- 64
- Merge medio
- 8 h 45 min
- PR fusionados (30 d)
- 36
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de quarto-dev/quarto
-
VS Code extension: `quarto preview` fails when the file path contains parentheses (path not shell-quoted)Posiblemente ocupada @Bondrewd-K la tomó hace 3 días. Abiertobug vscode
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
quarto-dev/quarto#1194 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
quarto-dev/quarto#1139 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 1-3 horas Aptitud para principiantes 90/100
quarto-dev/quarto#1138 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
quarto-dev/quarto#1137 ·
Los mantenedores suelen responder en 1 día
-
Add dependabot?Quizá libre de nuevo Un pull request para esta issue se cerró sin fusionarse. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
quarto-dev/quarto#1135 ·
Los mantenedores suelen responder en 1 día
Todos los issues de quarto-dev/quarto
Issues similares
-
bug engine
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
frostney/GocciaScript#1746 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 67/100
agilepathway/label-checker#739 · 4 comentarios ·
Los mantenedores suelen responder en 1 día
-
Wrong key location at startupAbierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 68/100
CopilotKit/OpenBot#800 ·
Los mantenedores suelen responder en 1 día
-
Endpoint links in the tag operations list are below the 24px target sizePosiblemente ocupada @AdzerKI la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 79/100
ehmpathy/rhachet-roles-bhuild#471 ·
Los mantenedores suelen responder en 1 día