Skip to content

Commit

Permalink
Fix for markdown cells (#2739)
Browse files Browse the repository at this point in the history
The latest update for the notebook cell handler broke cells in markdown.
Also implements notebook scope handler with a one of scope handler the
same way we do for collection item. We can have both language specific
implementations as well as the ide notebook api so this is a more proper
implementation.

## Checklist

- [x] I have added
[tests](https://www.cursorless.org/docs/contributing/test-case-recorder/)
- [/] I have updated the
[docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and
[cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet)
- [/] I have not broken the cheatsheet

---------

Co-authored-by: Phil Cohen <[email protected]>
  • Loading branch information
AndreasArvidsson and phillco authored Jan 21, 2025
1 parent 876694f commit 4688ad8
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 107 deletions.
27 changes: 27 additions & 0 deletions data/fixtures/recorded/languages/markdown/changeCell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
languageId: markdown
command:
version: 7
spokenForm: change cell
action:
name: clearAndSetSelection
target:
type: primitive
modifiers:
- type: containingScope
scopeType: {type: notebookCell}
usePrePhraseSnapshot: false
initialState:
documentContents: |
```
code
```
selections:
- anchor: {line: 1, character: 0}
active: {line: 1, character: 0}
marks: {}
finalState:
documentContents: |+
selections:
- anchor: {line: 0, character: 0}
active: {line: 0, character: 0}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class LineScopeHandler extends BaseScopeHandler {
type: "paragraph",
} as const;
protected readonly isHierarchical = false;
public readonly includeAdjacentInEvery: boolean = true;
public readonly includeAdjacentInEvery = true;

constructor(_scopeType: ScopeType, _languageId: string) {
super();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
Range,
type Direction,
type NotebookCell,
type Position,
type TextEditor,
} from "@cursorless/common";
import { ide } from "../../../singletons/ide.singleton";
import { NotebookCellTarget } from "../../targets";
import { BaseScopeHandler } from "./BaseScopeHandler";
import type { TargetScope } from "./scope.types";
import type { ScopeIteratorRequirements } from "./scopeHandler.types";

/**
* This is the scope handler for the actual notebook API in the IDE.
*/
export class NotebookCellApiScopeHandler extends BaseScopeHandler {
public readonly scopeType = { type: "notebookCell" } as const;
public readonly iterationScopeType = { type: "document" } as const;
protected isHierarchical = false;

constructor() {
super();
}

*generateScopeCandidates(
editor: TextEditor,
position: Position,
direction: Direction,
hints: ScopeIteratorRequirements,
): Iterable<TargetScope> {
const cells = getNotebookCells(editor, position, direction, hints);

for (const cell of cells) {
yield createTargetScope(cell);
}
}
}

function getNotebookCells(
editor: TextEditor,
position: Position,
direction: Direction,
hints: ScopeIteratorRequirements,
) {
const nb = getNotebook(editor);

if (nb == null) {
return [];
}

const { notebook, cell } = nb;

if (hints.containment === "required") {
return [cell];
}

if (
hints.containment === "disallowed" ||
hints.containment === "disallowedIfStrict"
) {
return direction === "forward"
? notebook.cells.slice(cell.index + 1)
: notebook.cells.slice(0, cell.index).reverse();
}

// Every scope
if (hints.distalPosition != null) {
const searchRange = new Range(position, hints.distalPosition);
if (searchRange.isRangeEqual(editor.document.range)) {
return notebook.cells;
}
}

return direction === "forward"
? notebook.cells.slice(cell.index)
: notebook.cells.slice(0, cell.index + 1).reverse();
}

function getNotebook(editor: TextEditor) {
const uri = editor.document.uri.toString();
for (const notebook of ide().visibleNotebookEditors) {
for (const cell of notebook.cells) {
if (cell.document.uri.toString() === uri) {
return { notebook, cell };
}
}
}
return undefined;
}

function createTargetScope(cell: NotebookCell): TargetScope {
const editor = getEditor(cell);
const contentRange = editor.document.range;
return {
editor,
domain: contentRange,
getTargets: (isReversed: boolean) => [
new NotebookCellTarget({
editor,
isReversed,
contentRange,
}),
],
};
}

function getEditor(cell: NotebookCell) {
const uri = cell.document.uri.toString();
for (const editor of ide().visibleTextEditors) {
if (editor.document.uri.toString() === uri) {
return editor;
}
}
throw new Error("Editor not found notebook cell");
}
Original file line number Diff line number Diff line change
@@ -1,132 +1,70 @@
import {
Range,
type Direction,
type NotebookCell,
type Position,
type ScopeType,
type TextEditor,
} from "@cursorless/common";
import type { LanguageDefinitions } from "../../../languages/LanguageDefinitions";
import { ide } from "../../../singletons/ide.singleton";
import { NotebookCellTarget } from "../../targets";
import { BaseScopeHandler } from "./BaseScopeHandler";
import { NotebookCellApiScopeHandler } from "./NotebookCellApiScopeHandler";
import { OneOfScopeHandler } from "./OneOfScopeHandler";
import type { TargetScope } from "./scope.types";
import type {
ComplexScopeType,
ScopeHandler,
ScopeIteratorRequirements,
} from "./scopeHandler.types";
import type { ScopeHandlerFactory } from "./ScopeHandlerFactory";

export class NotebookCellScopeHandler implements ScopeHandler {
export class NotebookCellScopeHandler extends BaseScopeHandler {
public readonly scopeType = { type: "notebookCell" } as const;
public readonly iterationScopeType = { type: "document" } as const;
public readonly includeAdjacentInEvery = false;
protected isHierarchical = false;
private readonly scopeHandler: ScopeHandler;

constructor(
private languageDefinitions: LanguageDefinitions,
_scopeType: ScopeType,
private languageId: string,
) {}

*generateScopes(
editor: TextEditor,
position: Position,
direction: Direction,
hints: ScopeIteratorRequirements,
): Iterable<TargetScope> {
const scopeHandler = this.languageDefinitions
.get(this.languageId)
?.getScopeHandler(this.scopeType);

if (scopeHandler != null) {
yield* scopeHandler.generateScopeCandidates(
editor,
position,
direction,
hints,
);
}

const cells = getNotebookCells(editor, position, direction, hints);

for (const cell of cells) {
yield createTargetScope(cell);
}
}
}

function getNotebookCells(
editor: TextEditor,
position: Position,
direction: Direction,
hints: ScopeIteratorRequirements,
) {
const nb = getNotebook(editor);

if (nb == null) {
return [];
}

const { notebook, cell } = nb;

if (hints.containment === "required") {
return [cell];
get iterationScopeType(): ScopeType | ComplexScopeType {
return this.scopeHandler.iterationScopeType;
}

if (
hints.containment === "disallowed" ||
hints.containment === "disallowedIfStrict"
constructor(
scopeHandlerFactory: ScopeHandlerFactory,
languageDefinitions: LanguageDefinitions,
_scopeType: ScopeType,
languageId: string,
) {
return direction === "forward"
? notebook.cells.slice(cell.index + 1)
: notebook.cells.slice(0, cell.index).reverse();
}
super();

// Every scope
if (hints.distalPosition != null) {
const searchRange = new Range(position, hints.distalPosition);
if (searchRange.isRangeEqual(editor.document.range)) {
return notebook.cells;
}
}
this.scopeHandler = (() => {
const apiScopeHandler = new NotebookCellApiScopeHandler();

return direction === "forward"
? notebook.cells.slice(cell.index)
: notebook.cells.slice(0, cell.index + 1).reverse();
}
const languageScopeHandler = languageDefinitions
.get(languageId)
?.getScopeHandler(this.scopeType);

function getNotebook(editor: TextEditor) {
const uri = editor.document.uri.toString();
for (const notebook of ide().visibleNotebookEditors) {
for (const cell of notebook.cells) {
if (cell.document.uri.toString() === uri) {
return { notebook, cell };
if (languageScopeHandler == null) {
return apiScopeHandler;
}
}
}
return undefined;
}

function createTargetScope(cell: NotebookCell): TargetScope {
const editor = getEditor(cell);
const contentRange = editor.document.range;
return {
editor,
domain: contentRange,
getTargets: (isReversed: boolean) => [
new NotebookCellTarget({
editor,
isReversed,
contentRange,
}),
],
};
}
return OneOfScopeHandler.createFromScopeHandlers(
scopeHandlerFactory,
{
type: "oneOf",
scopeTypes: [
languageScopeHandler.scopeType,
apiScopeHandler.scopeType,
],
},
[languageScopeHandler, apiScopeHandler],
languageId,
);
})();
}

function getEditor(cell: NotebookCell) {
const uri = cell.document.uri.toString();
for (const editor of ide().visibleTextEditors) {
if (editor.document.uri.toString() === uri) {
return editor;
}
generateScopeCandidates(
editor: TextEditor,
position: Position,
direction: Direction,
hints: ScopeIteratorRequirements,
): Iterable<TargetScope> {
return this.scopeHandler.generateScopes(editor, position, direction, hints);
}
throw new Error("Editor not found notebook cell");
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export class ScopeHandlerFactoryImpl implements ScopeHandlerFactory {
);
case "notebookCell":
return new NotebookCellScopeHandler(
this,
this.languageDefinitions,
scopeType,
languageId,
Expand Down

0 comments on commit 4688ad8

Please sign in to comment.