Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/common/src/types/ScopeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ export interface ScopeProvider {
config: ScopeRangeConfig,
) => ScopeRanges[];

/**
* Get the scope ranges for the given editor and range.
* @param editor The editor
* @param scopeType The scope type to get ranges for
* @param range The range to get scope ranges for
* @returns A list of scope ranges
*/
provideScopeRangesForRange(
editor: TextEditor,
scopeType: ScopeType,
range: Range,
): ScopeRanges[];

/**
* Get the iteration scope ranges for the given editor.
* @param editor The editor
Expand Down
1 change: 1 addition & 0 deletions packages/cursorless-engine/src/cursorlessEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ function createScopeProvider(

return {
provideScopeRanges: rangeProvider.provideScopeRanges,
provideScopeRangesForRange: rangeProvider.provideScopeRangesForRange,
provideIterationScopeRanges: rangeProvider.provideIterationScopeRanges,
onDidChangeScopeRanges: rangeWatcher.onDidChangeScopeRanges,
onDidChangeIterationScopeRanges:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Range, TextEditor } from "@cursorless/common";

import { getRangeLength } from "../../../../util/rangeUtils";

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import type {
IterationScopeRanges,
ScopeRangeConfig,
ScopeRanges,
ScopeType,
TextEditor,
} from "@cursorless/common";

import { Range } from "@cursorless/common";
import type { ModifierStageFactory } from "../processTargets/ModifierStageFactory";
import type { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory";
import { getIterationRange } from "./getIterationRange";
Expand All @@ -21,6 +22,8 @@ export class ScopeRangeProvider {
private modifierStageFactory: ModifierStageFactory,
) {
this.provideScopeRanges = this.provideScopeRanges.bind(this);
this.provideScopeRangesForRange =
this.provideScopeRangesForRange.bind(this);
this.provideIterationScopeRanges =
this.provideIterationScopeRanges.bind(this);
}
Expand All @@ -45,6 +48,32 @@ export class ScopeRangeProvider {
);
}

provideScopeRangesForRange(
editor: TextEditor,
scopeType: ScopeType,
range: Range,
): ScopeRanges[] {
const scopeHandler = this.scopeHandlerFactory.maybeCreate(
scopeType,
editor.document.languageId,
);

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

// Need to have a non empty intersection with the scopes
if (range.isEmpty) {
const offset = editor.document.offsetAt(range.start);
range = new Range(
editor.document.positionAt(offset - 1),
editor.document.positionAt(offset + 1),
);
}

return getScopeRanges(editor, scopeHandler, range);
}

provideIterationScopeRanges(
editor: TextEditor,
{ scopeType, visibleOnly, includeNestedTargets }: IterationScopeRangeConfig,
Expand Down
13 changes: 9 additions & 4 deletions packages/cursorless-engine/src/util/rangeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@ export function expandToFullLine(editor: TextEditor, range: Range) {
}

export function getRangeLength(editor: TextEditor, range: Range) {
return range.isEmpty
? 0
: editor.document.offsetAt(range.end) -
editor.document.offsetAt(range.start);
if (range.isEmpty) {
return 0;
}
if (range.isSingleLine) {
return range.end.character - range.start.character;
}
return (
editor.document.offsetAt(range.end) - editor.document.offsetAt(range.start)
);
}

/**
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 23 additions & 1 deletion packages/cursorless-org-docs/src/docs/user/scope-sidebar.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
# The Cursorless sidebar

You can say `"bar cursorless"` to show the Cursorless sidebar. Currently, the sidebar just contains a section showing a list of all scopes, organized by whether they are present and supported in the active text editor. As you type, the list of present scopes will update in real time. Clicking on a scope will visualize it using the [scope visualizer](scope-visualizer.md). Note that for legacy scopes, we can't tell whether they are present in the active text editor, so we list them under a separate Legacy section. Clicking on these scopes will not visualize them, as we also don't support visualizing legacy scopes.
You can say `"bar cursorless"` to show the Cursorless sidebar.

## Scopes

- Displays all available scopes, grouped by whether they are currently present and supported in the active text editor.
- The list updates in real time as you type or move your selection.
- Clicking a scope highlights it using the [scope visualizer](scope-visualizer.md).
- Shows your custom spoken forms for scopes.

### Scope icons

To identify the scope for a piece of code:

1. First select the code in your editor.
2. Then look in the sidebar for the following icons:\
🎯 The scope exactly matches your selection\
📦 The scope contains your selection

![sidebar scopes](./images/sidebar-scopes.png)

## Tutorial

Interactive tutorial to learn Cursorless
68 changes: 61 additions & 7 deletions packages/cursorless-vscode/src/ScopeTreeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import type {
CursorlessCommandId,
ScopeProvider,
ScopeSupportInfo,
ScopeType,
ScopeTypeInfo,
Selection,
TextEditor,
} from "@cursorless/common";
import {
CURSORLESS_SCOPE_TREE_VIEW_ID,
Expand All @@ -12,8 +15,11 @@ import {
serializeScopeType,
uriEncodeHashId,
} from "@cursorless/common";
import type { CustomSpokenFormGenerator } from "@cursorless/cursorless-engine";
import type { VscodeApi } from "@cursorless/vscode-common";
import {
ide,
type CustomSpokenFormGenerator,
} from "@cursorless/cursorless-engine";
import { type VscodeApi } from "@cursorless/vscode-common";
import { isEqual } from "lodash-es";
import type {
Disposable,
Expand Down Expand Up @@ -79,7 +85,7 @@ export class ScopeTreeProvider implements TreeDataProvider<MyTreeItem> {
}
}

onDidChangeVisible(e: TreeViewVisibilityChangeEvent) {
private onDidChangeVisible(e: TreeViewVisibilityChangeEvent) {
if (e.visible) {
if (this.visibleDisposable != null) {
return;
Expand All @@ -105,6 +111,9 @@ export class ScopeTreeProvider implements TreeDataProvider<MyTreeItem> {
this.scopeVisualizer.onDidChangeScopeType(() => {
this._onDidChangeTreeData.fire();
}),
this.vscodeApi.window.onDidChangeTextEditorSelection(() => {
this._onDidChangeTreeData.fire();
}),
);
}

Expand Down Expand Up @@ -160,6 +169,20 @@ export class ScopeTreeProvider implements TreeDataProvider<MyTreeItem> {
private getScopeTypesWithSupport(
scopeSupport: ScopeSupport,
): ScopeSupportTreeItem[] {
const getContainmentIcon = (() => {
if (scopeSupport !== ScopeSupport.supportedAndPresentInEditor) {
return null;
}
const editor = ide().activeTextEditor;
if (editor == null || editor.selections.length !== 1) {
return null;
}
const selection = editor.selections[0];
return (scopeType: ScopeType) => {
return this.getContainmentIcon(editor, selection, scopeType);
};
})();

return this.supportLevels
.filter(
(supportLevel) =>
Expand All @@ -177,6 +200,7 @@ export class ScopeTreeProvider implements TreeDataProvider<MyTreeItem> {
new ScopeSupportTreeItem(
supportLevel,
isEqual(supportLevel.scopeType, this.scopeVisualizer.scopeType),
getContainmentIcon?.(supportLevel.scopeType),
),
)
.sort((a, b) => {
Expand All @@ -200,6 +224,33 @@ export class ScopeTreeProvider implements TreeDataProvider<MyTreeItem> {
});
}

private getContainmentIcon(
editor: TextEditor,
selection: Selection,
scopeType: ScopeType,
): string | undefined {
const scopes = this.scopeProvider.provideScopeRangesForRange(
editor,
scopeType,
selection,
);

for (const scope of scopes) {
for (const target of scope.targets) {
// Scope target exactly matches selection
if (target.contentRange.isRangeEqual(selection)) {
return "🎯";
}
// Scope target contains selection
if (target.contentRange.contains(selection)) {
return "📦";
}
}
}

return undefined;
}

dispose() {
this.visibleDisposable?.dispose();
}
Expand All @@ -225,6 +276,7 @@ class ScopeSupportTreeItem extends TreeItem {
constructor(
public readonly scopeTypeInfo: ScopeTypeInfo,
isVisualized: boolean,
containmentIcon: string | undefined,
) {
let label: string;
let tooltip: string;
Expand All @@ -250,7 +302,10 @@ class ScopeSupportTreeItem extends TreeItem {
);

this.tooltip = tooltip == null ? tooltip : new MarkdownString(tooltip);
this.description = scopeTypeInfo.humanReadableName;
this.description =
containmentIcon != null
? `${containmentIcon} ${scopeTypeInfo.humanReadableName}`
: scopeTypeInfo.humanReadableName;

this.command = isVisualized
? {
Expand All @@ -271,9 +326,8 @@ class ScopeSupportTreeItem extends TreeItem {
if (scopeTypeInfo.isLanguageSpecific) {
const languageId = window.activeTextEditor?.document.languageId;
if (languageId != null) {
const fileExtension = getLanguageExtensionSampleFromLanguageId(
window.activeTextEditor!.document.languageId,
);
const fileExtension =
getLanguageExtensionSampleFromLanguageId(languageId);
if (fileExtension != null) {
this.resourceUri = URI.parse(
"cursorless-dummy://dummy/dummy" + fileExtension,
Expand Down
Loading