From 205d55b5adb352f1039e1f5b456409f840cf1750 Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Mon, 17 Jul 2023 14:29:28 +0100 Subject: [PATCH 1/2] Add `ScopeSupportChecker` --- packages/common/src/util/itertools.ts | 16 +++ .../ScopeVisualizer/ScopeSupportChecker.ts | 110 ++++++++++++++++++ .../src/api/ScopeProvider.ts | 29 +++++ .../cursorless-engine/src/cursorlessEngine.ts | 4 + 4 files changed, 159 insertions(+) create mode 100644 packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts diff --git a/packages/common/src/util/itertools.ts b/packages/common/src/util/itertools.ts index 5324763e23..398baf0689 100644 --- a/packages/common/src/util/itertools.ts +++ b/packages/common/src/util/itertools.ts @@ -50,3 +50,19 @@ export function partition( } return [first, second]; } + +/** + * Returns `true` if the given iterable is empty, `false` otherwise + * + * From https://github.com/sindresorhus/is-empty-iterable/blob/12d3b4f966170d9d85a2067f5326668d5bb910a0/index.js + * @param iterable The iterable to check + * @returns `true` if the iterable is empty, `false` otherwise + */ +export function isEmptyIterable(iterable: Iterable): boolean { + for (const _ of iterable) { + // eslint-disable-line no-unused-vars, no-unreachable-loop + return false; + } + + return true; +} diff --git a/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts new file mode 100644 index 0000000000..d9ee8f1664 --- /dev/null +++ b/packages/cursorless-engine/src/ScopeVisualizer/ScopeSupportChecker.ts @@ -0,0 +1,110 @@ +import { + Position, + ScopeType, + SimpleScopeTypeType, + TextEditor, + isEmptyIterable, +} from "@cursorless/common"; +import { LegacyLanguageId } from "../languages/LegacyLanguageId"; +import { languageMatchers } from "../languages/getNodeMatcher"; +import { ScopeHandlerFactory } from "../processTargets/modifiers/scopeHandlers/ScopeHandlerFactory"; +import { ScopeHandler } from "../processTargets/modifiers/scopeHandlers/scopeHandler.types"; +import { ScopeSupport } from "../api/ScopeProvider"; + +/** + * Determines the level of support for a given scope type in a given editor. + * This is primarily determined by the language id of the editor, though some + * scopes are supported in all languages. + */ +export class ScopeSupportChecker { + constructor(private scopeHandlerFactory: ScopeHandlerFactory) { + this.getScopeSupport = this.getScopeSupport.bind(this); + this.getIterationScopeSupport = this.getIterationScopeSupport.bind(this); + } + + /** + * Determine the level of support for {@link scopeType} in {@link editor}, as + * determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for {@link scopeType} in {@link editor} + */ + getScopeSupport(editor: TextEditor, scopeType: ScopeType): ScopeSupport { + const { languageId } = editor.document; + const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId); + + if (scopeHandler == null) { + return getLegacyScopeSupport(languageId, scopeType); + } + + return editorContainsScope(editor, scopeHandler) + ? ScopeSupport.supportedAndPresentInEditor + : ScopeSupport.supportedButNotPresentInEditor; + } + + /** + * Determine the level of support for the iteration scope of {@link scopeType} + * in {@link editor}, as determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for the iteration scope of {@link scopeType} + * in {@link editor} + */ + getIterationScopeSupport( + editor: TextEditor, + scopeType: ScopeType, + ): ScopeSupport { + const { languageId } = editor.document; + const scopeHandler = this.scopeHandlerFactory.create(scopeType, languageId); + + if (scopeHandler == null) { + return getLegacyScopeSupport(languageId, scopeType); + } + + const iterationScopeHandler = this.scopeHandlerFactory.create( + scopeHandler.iterationScopeType, + languageId, + ); + + if (iterationScopeHandler == null) { + return ScopeSupport.unsupported; + } + + return editorContainsScope(editor, iterationScopeHandler) + ? ScopeSupport.supportedAndPresentInEditor + : ScopeSupport.supportedButNotPresentInEditor; + } +} + +function editorContainsScope( + editor: TextEditor, + scopeHandler: ScopeHandler, +): boolean { + return !isEmptyIterable( + scopeHandler.generateScopes(editor, new Position(0, 0), "forward"), + ); +} + +function getLegacyScopeSupport( + languageId: string, + scopeType: ScopeType, +): ScopeSupport { + switch (scopeType.type) { + case "boundedNonWhitespaceSequence": + case "surroundingPair": + return ScopeSupport.supportedLegacy; + case "notebookCell": + // FIXME: What to do here + return ScopeSupport.unsupported; + default: + if ( + languageMatchers[languageId as LegacyLanguageId]?.[ + scopeType.type as SimpleScopeTypeType + ] != null + ) { + return ScopeSupport.supportedLegacy; + } + + return ScopeSupport.unsupported; + } +} diff --git a/packages/cursorless-engine/src/api/ScopeProvider.ts b/packages/cursorless-engine/src/api/ScopeProvider.ts index 7ea904ae7c..40d4bd6916 100644 --- a/packages/cursorless-engine/src/api/ScopeProvider.ts +++ b/packages/cursorless-engine/src/api/ScopeProvider.ts @@ -53,6 +53,28 @@ export interface ScopeProvider { callback: IterationScopeChangeEventCallback, config: IterationScopeRangeConfig, ) => Disposable; + + /** + * Determine the level of support for {@link scopeType} in {@link editor}, as + * determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for {@link scopeType} in {@link editor} + */ + getScopeSupport: (editor: TextEditor, scopeType: ScopeType) => ScopeSupport; + + /** + * Determine the level of support for the iteration scope of {@link scopeType} + * in {@link editor}, as determined by its language id. + * @param editor The editor to check + * @param scopeType The scope type to check + * @returns The level of support for the iteration scope of {@link scopeType} + * in {@link editor} + */ + getIterationScopeSupport: ( + editor: TextEditor, + scopeType: ScopeType, + ) => ScopeSupport; } interface ScopeRangeConfigBase { @@ -131,3 +153,10 @@ export interface IterationScopeRanges { targets?: TargetRanges[]; }[]; } + +export enum ScopeSupport { + supportedAndPresentInEditor, + supportedButNotPresentInEditor, + supportedLegacy, + unsupported, +} diff --git a/packages/cursorless-engine/src/cursorlessEngine.ts b/packages/cursorless-engine/src/cursorlessEngine.ts index 4d7c6594b1..e1265749bb 100644 --- a/packages/cursorless-engine/src/cursorlessEngine.ts +++ b/packages/cursorless-engine/src/cursorlessEngine.ts @@ -3,6 +3,7 @@ import { StoredTargetMap, TestCaseRecorder, TreeSitter } from "."; import { CursorlessEngine } from "./api/CursorlessEngineApi"; import { ScopeProvider } from "./api/ScopeProvider"; import { ScopeRangeProvider } from "./ScopeVisualizer/ScopeRangeProvider"; +import { ScopeSupportChecker } from "./ScopeVisualizer/ScopeSupportChecker"; import { Debug } from "./core/Debug"; import { HatTokenMapImpl } from "./core/HatTokenMapImpl"; import { Snippets } from "./core/Snippets"; @@ -101,6 +102,7 @@ function createScopeProvider( ); const rangeWatcher = new ScopeRangeWatcher(rangeProvider); + const supportChecker = new ScopeSupportChecker(scopeHandlerFactory); return { provideScopeRanges: rangeProvider.provideScopeRanges, @@ -108,5 +110,7 @@ function createScopeProvider( onDidChangeScopeRanges: rangeWatcher.onDidChangeScopeRanges, onDidChangeIterationScopeRanges: rangeWatcher.onDidChangeIterationScopeRanges, + getScopeSupport: supportChecker.getScopeSupport, + getIterationScopeSupport: supportChecker.getIterationScopeSupport, }; } From 33ea4f2640273a6db8d746f7e301a0593f317f9a Mon Sep 17 00:00:00 2001 From: Pokey Rule <755842+pokey@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:21:25 +0100 Subject: [PATCH 2/2] Remove lint override --- packages/common/src/util/itertools.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/common/src/util/itertools.ts b/packages/common/src/util/itertools.ts index 398baf0689..435b3d044c 100644 --- a/packages/common/src/util/itertools.ts +++ b/packages/common/src/util/itertools.ts @@ -60,7 +60,6 @@ export function partition( */ export function isEmptyIterable(iterable: Iterable): boolean { for (const _ of iterable) { - // eslint-disable-line no-unused-vars, no-unreachable-loop return false; }