Skip to content

Commit be61609

Browse files
Added playground action (#1894)
Fixes #1893 `"parse tree class"` ![image](https://github.com/cursorless-dev/cursorless/assets/3511326/d5dfcae1-10a2-46bf-8f42-f75a680ddd63) ## 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: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 11f9de7 commit be61609

File tree

13 files changed

+222
-8
lines changed

13 files changed

+222
-8
lines changed

cursorless-talon-dev/src/cursorless_dev.talon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mode: command
2+
mode: user.cursorless_spoken_form_test
13
tag: user.cursorless
24
-
35

@@ -28,3 +30,6 @@ tag: user.cursorless
2830

2931
test snippet make <user.cursorless_target>:
3032
user.private_cursorless_make_snippet_test(cursorless_target)
33+
34+
parse tree <user.cursorless_target>:
35+
user.cursorless_command("private.showParseTree", cursorless_target)

packages/common/src/ide/PassthroughIDEBase.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import {
1010
TextEditorVisibleRangesChangeEvent,
1111
} from "./types/events.types";
1212
import { FlashDescriptor } from "./types/FlashDescriptor";
13-
import { Disposable, IDE, RunMode, WorkspaceFolder } from "./types/ide.types";
13+
import {
14+
Disposable,
15+
IDE,
16+
OpenUntitledTextDocumentOptions,
17+
RunMode,
18+
WorkspaceFolder,
19+
} from "./types/ide.types";
1420
import { Messages } from "./types/Messages";
1521
import { QuickPickOptions } from "./types/QuickPickOptions";
1622
import { State } from "./types/State";
@@ -137,6 +143,12 @@ export default class PassthroughIDEBase implements IDE {
137143
return this.original.openTextDocument(path);
138144
}
139145

146+
public openUntitledTextDocument(
147+
options?: OpenUntitledTextDocumentOptions,
148+
): Promise<TextEditor> {
149+
return this.original.openUntitledTextDocument(options);
150+
}
151+
140152
public showQuickPick(
141153
items: readonly string[],
142154
options?: QuickPickOptions,

packages/common/src/ide/fake/FakeIDE.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
import type { EditableTextEditor, TextEditor } from "../..";
21
import { pull } from "lodash";
2+
import type { EditableTextEditor, TextEditor } from "../..";
33
import { GeneralizedRange } from "../../types/GeneralizedRange";
44
import { TextDocument } from "../../types/TextDocument";
55
import type { TextDocumentChangeEvent } from "../types/Events";
6+
import { FlashDescriptor } from "../types/FlashDescriptor";
7+
import { QuickPickOptions } from "../types/QuickPickOptions";
68
import {
79
Event,
810
TextEditorSelectionChangeEvent,
911
TextEditorVisibleRangesChangeEvent,
1012
} from "../types/events.types";
11-
import { FlashDescriptor } from "../types/FlashDescriptor";
1213
import type {
1314
Disposable,
1415
IDE,
16+
OpenUntitledTextDocumentOptions,
1517
RunMode,
1618
WorkspaceFolder,
1719
} from "../types/ide.types";
18-
import { QuickPickOptions } from "../types/QuickPickOptions";
1920
import { FakeCapabilities } from "./FakeCapabilities";
2021
import FakeClipboard from "./FakeClipboard";
2122
import FakeConfiguration from "./FakeConfiguration";
@@ -92,6 +93,12 @@ export default class FakeIDE implements IDE {
9293
throw Error("Not implemented");
9394
}
9495

96+
public openUntitledTextDocument(
97+
_options: OpenUntitledTextDocumentOptions,
98+
): Promise<TextEditor> {
99+
throw Error("Not implemented");
100+
}
101+
95102
public setQuickPickReturnValue(value: string | undefined) {
96103
this.quickPickReturnValue = value;
97104
}

packages/common/src/ide/types/ide.types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import { State } from "./State";
2222

2323
export type RunMode = "production" | "development" | "test";
2424
export type HighlightId = string;
25+
export interface OpenUntitledTextDocumentOptions {
26+
language?: string;
27+
content?: string;
28+
}
2529

2630
export interface IDE {
2731
readonly configuration: Configuration;
@@ -105,6 +109,17 @@ export interface IDE {
105109
*/
106110
openTextDocument(path: string): Promise<TextEditor>;
107111

112+
/**
113+
* Opens an untitled document.
114+
*
115+
* @see {@link openTextDocument}
116+
* @param options optional language and documents content
117+
* @return An editor
118+
*/
119+
openUntitledTextDocument(
120+
options?: OpenUntitledTextDocumentOptions,
121+
): Promise<TextEditor>;
122+
108123
/**
109124
* An event that is emitted when a {@link TextDocument text document} is opened or when the language id
110125
* of a text document {@link languages.setTextDocumentLanguage has been changed}.

packages/common/src/types/command/ActionDescriptor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const simpleActionNames = [
4646
"toggleLineBreakpoint",
4747
"toggleLineComment",
4848
"unfoldRegion",
49+
"private.showParseTree",
4950
"private.getTargets",
5051
] as const;
5152

packages/cursorless-engine/src/actions/Actions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TreeSitter } from "..";
12
import { Snippets } from "../core/Snippets";
23
import { RangeUpdater } from "../core/updateSelections/RangeUpdater";
34
import { ModifierStageFactory } from "../processTargets/ModifierStageFactory";
@@ -26,6 +27,7 @@ import {
2627
} from "./InsertEmptyLines";
2728
import InsertSnippet from "./InsertSnippet";
2829
import { PasteFromClipboard } from "./PasteFromClipboard";
30+
import ShowParseTree from "./ShowParseTree";
2931
import Remove from "./Remove";
3032
import Replace from "./Replace";
3133
import Rewrap from "./Rewrap";
@@ -63,6 +65,7 @@ import { ActionRecord } from "./actions.types";
6365
*/
6466
export class Actions implements ActionRecord {
6567
constructor(
68+
private treeSitter: TreeSitter,
6669
private snippets: Snippets,
6770
private rangeUpdater: RangeUpdater,
6871
private modifierStageFactory: ModifierStageFactory,
@@ -145,5 +148,6 @@ export class Actions implements ActionRecord {
145148
this.snippets,
146149
this.modifierStageFactory,
147150
);
151+
["private.showParseTree"] = new ShowParseTree(this.treeSitter);
148152
["private.getTargets"] = new GetTargets();
149153
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { FlashStyle, Range, TextDocument } from "@cursorless/common";
2+
import * as path from "node:path";
3+
import type { Tree, TreeCursor } from "web-tree-sitter";
4+
import type { TreeSitter } from "..";
5+
import { ide } from "../singletons/ide.singleton";
6+
import type { Target } from "../typings/target.types";
7+
import { flashTargets } from "../util/targetUtils";
8+
import type { ActionReturnValue } from "./actions.types";
9+
10+
export default class ShowParseTree {
11+
constructor(private treeSitter: TreeSitter) {
12+
this.run = this.run.bind(this);
13+
}
14+
15+
async run(targets: Target[]): Promise<ActionReturnValue> {
16+
await flashTargets(ide(), targets, FlashStyle.referenced);
17+
18+
const results: string[] = ["# Cursorless parse tree"];
19+
20+
for (const target of targets) {
21+
const { editor, contentRange } = target;
22+
const tree = this.treeSitter.getTree(editor.document);
23+
results.push(parseTree(editor.document, tree, contentRange));
24+
}
25+
26+
ide().openUntitledTextDocument({
27+
language: "markdown",
28+
content: results.join("\n\n"),
29+
});
30+
31+
return { thatTargets: targets };
32+
}
33+
}
34+
35+
function parseTree(
36+
document: TextDocument,
37+
tree: Tree,
38+
contentRange: Range,
39+
): string {
40+
const resultPlayground: string[] = [];
41+
const resultQuery: string[] = [];
42+
43+
parseCursor(resultPlayground, resultQuery, contentRange, tree.walk(), 0);
44+
45+
return [
46+
`## ${path.basename(document.uri.path)} [${contentRange}]\n`,
47+
`\`\`\`${document.languageId}`,
48+
document.getText(contentRange),
49+
"```",
50+
"",
51+
"```scm",
52+
...resultQuery,
53+
"```",
54+
"",
55+
"```js",
56+
...resultPlayground,
57+
"```",
58+
"",
59+
].join("\n");
60+
}
61+
62+
function parseCursor(
63+
resultPlayground: string[],
64+
resultQuery: string[],
65+
contentRange: Range,
66+
cursor: TreeCursor,
67+
numIndents: number,
68+
): void {
69+
while (true) {
70+
const nodeRange = new Range(
71+
cursor.startPosition.row,
72+
cursor.startPosition.column,
73+
cursor.endPosition.row,
74+
cursor.endPosition.column,
75+
);
76+
77+
if (contentRange.intersection(nodeRange) != null) {
78+
const indentation = " ".repeat(numIndents);
79+
const fieldName = getFieldName(cursor);
80+
const prefix = indentation + fieldName;
81+
82+
// Named node
83+
if (cursor.nodeIsNamed) {
84+
resultPlayground.push(`${prefix}${cursor.nodeType} [${nodeRange}]`);
85+
resultQuery.push(`${prefix}(${cursor.nodeType}`);
86+
87+
// Named node with children
88+
if (cursor.gotoFirstChild()) {
89+
parseCursor(
90+
resultPlayground,
91+
resultQuery,
92+
contentRange,
93+
cursor,
94+
numIndents + 1,
95+
);
96+
cursor.gotoParent();
97+
resultQuery.push(`${indentation})`);
98+
}
99+
// Named node without children
100+
else {
101+
resultQuery[resultQuery.length - 1] += ")";
102+
}
103+
}
104+
// Anonymous node
105+
else {
106+
const type = `"${cursor.nodeType}"`;
107+
resultPlayground.push(`${prefix}${type} [${nodeRange}]`);
108+
resultQuery.push(`${prefix}${type}`);
109+
}
110+
}
111+
112+
if (!cursor.gotoNextSibling()) {
113+
return;
114+
}
115+
}
116+
}
117+
118+
function getFieldName(cursor: TreeCursor): string {
119+
const field = cursor.currentFieldName();
120+
return field != null ? `${field}: ` : "";
121+
}

packages/cursorless-engine/src/cursorlessEngine.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function createCursorlessEngine(
5959
commandApi: {
6060
runCommand(command: Command) {
6161
return runCommand(
62+
treeSitter,
6263
debug,
6364
hatTokenMap,
6465
testCaseRecorder,
@@ -72,6 +73,7 @@ export function createCursorlessEngine(
7273

7374
runCommandSafe(...args: unknown[]) {
7475
return runCommand(
76+
treeSitter,
7577
debug,
7678
hatTokenMap,
7779
testCaseRecorder,

packages/cursorless-engine/src/generateSpokenForm/defaultSpokenForms/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const actions = {
5454
insertSnippet: "snippet",
5555
pasteFromClipboard: "paste",
5656

57+
["private.showParseTree"]: "parse tree",
5758
["experimental.setInstanceReference"]: "from",
5859

5960
editNew: null,

packages/cursorless-engine/src/runCommand.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Snippets } from "./core/Snippets";
66
import { CommandRunnerImpl } from "./core/commandRunner/CommandRunnerImpl";
77
import { canonicalizeAndValidateCommand } from "./core/commandVersionUpgrades/canonicalizeAndValidateCommand";
88
import { RangeUpdater } from "./core/updateSelections/RangeUpdater";
9-
import { StoredTargetMap, TestCaseRecorder } from "./index";
9+
import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "./index";
1010
import { LanguageDefinitions } from "./languages/LanguageDefinitions";
1111
import { TargetPipelineRunner } from "./processTargets";
1212
import { MarkStageFactoryImpl } from "./processTargets/MarkStageFactoryImpl";
@@ -26,6 +26,7 @@ import { ScopeHandlerFactoryImpl } from "./processTargets/modifiers/scopeHandler
2626
* 5. Call {@link CommandRunnerImpl.run} to run the actual command.
2727
*/
2828
export async function runCommand(
29+
treeSitter: TreeSitter,
2930
debug: Debug,
3031
hatTokenMap: HatTokenMap,
3132
testCaseRecorder: TestCaseRecorder,
@@ -47,6 +48,7 @@ export async function runCommand(
4748
);
4849

4950
let commandRunner = createCommandRunner(
51+
treeSitter,
5052
languageDefinitions,
5153
debug,
5254
storedTargets,
@@ -66,6 +68,7 @@ export async function runCommand(
6668
}
6769

6870
function createCommandRunner(
71+
treeSitter: TreeSitter,
6972
languageDefinitions: LanguageDefinitions,
7073
debug: Debug,
7174
storedTargets: StoredTargetMap,
@@ -92,6 +95,6 @@ function createCommandRunner(
9295
debug,
9396
storedTargets,
9497
targetPipelineRunner,
95-
new Actions(snippets, rangeUpdater, modifierStageFactory),
98+
new Actions(treeSitter, snippets, rangeUpdater, modifierStageFactory),
9699
);
97100
}

0 commit comments

Comments
 (0)