Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2778 (2024-04-21 01:40:15)

feat: Allow showing/suppressing the Auto-Suggest in custom editor implementations
<!--- Provide a general summary of your changes in the Title above -->

# Description

Kanban v2-beta now uses obsidian's native markdown editor. This means that getting tasks working in Kanban cards is fairly easy. This PR serves as an example of how this could be done. I'm not familiar with this code base, however, so not sure if this is the best way to implement Kanban support. Feel free to close this an implement in a different way if desired.

<!--- Describe your changes in detail -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

Folks have asked for the Kanban plugin to support tasks for a long time now, but it was challenging in the past due to the custom markdown editor that was used. I tried to add support 100% in the Kanban plugin, but there are some major hurdles to doing so. Specifically, kanban does not supply the tasklist formatting to the editor so the editor suggest thinks its in a plain markdown string.

## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

I've run `yarn test`. I also tested manually using Kanban version 2.0.18-beta which adds the `isKanbanEditor` flag to the Kanban editor.

## Screenshots (if appropriate)


<img width="288" alt="Screenshot 2024-04-20 at 6 24 38 PM" src="https://github.com/obsidian-tasks-group/obsidian-tasks/assets/2694747/26517e85-c76d-4656-a4c8-20827496d40d"><br>


<img width="400" alt="Screenshot 2024-04-20 at 6 36 56 PM" src="https://github.com/obsidian-tasks-group/obsidian-tasks/assets/2694747/ca21d80a-6f84-4d1f-b5ee-282dce4a25c3">


## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [ ] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)
- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)
- [ ] **Breaking change** (prefix: `feat!!` or `fix!!` - fix or feature that would cause existing functionality to not work as expected)
- [ ] **Documentation** (prefix: `docs` - improvements to any documentation content **for users**)
- [ ] **Sample vault** (prefix: `vault` - improvements to the [Tasks-Demo sample vault](https://github.com/obsidian-tasks-group/obsidian-tasks/tree/main/resources/sample_vaults/Tasks-Demo))
- [ ] **Contributing Guidelines** (prefix: `contrib` - any improvements to documentation content **for contributors** - see [Contributing to Tasks](https://publish.obsidian.md/tasks-contributing/))

Internal changes:

- [ ] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)
- [ ] **Infrastructure** (prefix: `chore` - examples include GitHub Actions, issue templates)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [ ] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).
    - I didn't add tests to handle the case of a Kanban editor, but I think this could be mocked fairly easily

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/docs/Advanced/Tasks Api.md b/docs/Advanced/Tasks Api.md
index 89ed4645..c1ea4a8e 100644
--- a/docs/Advanced/Tasks Api.md
+++ b/docs/Advanced/Tasks Api.md
@@ -103,3 +103,28 @@ Use these steps to make the following options appear (tested in QuickAdd 0.12.0)
 
 Screenshot of QuickAdd capture settings (example)
 ![Screenshot - Edit the QuickAdd Capture Configuration](../../images/api-create-taskline-modal-quickadd-capture-example.png)
+
+## Auto-Suggest Integration
+
+Plugins that [extend Obsidian's markdown editor](https://gist.github.com/Fevol/caa478ce303e69eabede7b12b2323838) can control if and when Tasks' [[Auto-Suggest]] displays by implementing a `showTasksPluginAutoSuggest` method on the extended editor class. This method must adhere the function definition below.
+
+```typescript
+/**
+ * Returns
+ * - true to explicitly request that the suggest be displayed
+ * - false to request that it be hidden
+ * - undefined to defer to Tasks' default behavior
+ *
+ * @param cursor The current cursor position in the editor
+ * @param editor The editor instance
+ * @param lineHasGlobalFilter True if the line the cursor is in matches the
+ *        global filter or if no global filter is set
+ */
+showTasksPluginAutoSuggest(
+  cursor: EditorPosition,
+  editor: Editor,
+  lineHasGlobalFilter: boolean
+): boolean | undefined
+```
+
+This can be used, for example, to display the Auto-Suggest on non-task lines. [See the Kanban plugin for an example](https://github.com/mgmeyers/obsidian-kanban/blob/5fa792b9c2157390fe493f0feed6f0bc9be72910/src/components/Editor/MarkdownEditor.tsx#L100-L106).
diff --git a/src/Suggestor/EditorSuggestorPopup.ts b/src/Suggestor/EditorSuggestorPopup.ts
index b25d1f56..bb7a157c 100644
--- a/src/Suggestor/EditorSuggestorPopup.ts
+++ b/src/Suggestor/EditorSuggestorPopup.ts
@@ -32,7 +32,7 @@ export class EditorSuggestor extends EditorSuggest<SuggestInfoWithContext> {
     onTrigger(cursor: EditorPosition, editor: Editor, _file: TFile): EditorSuggestTriggerInfo | null {
         if (!this.settings.autoSuggestInEditor) return null;
         const line = editor.getLine(cursor.line);
-        if (canSuggestForLine(line, cursor.ch)) {
+        if (canSuggestForLine(line, cursor, editor)) {
             return {
                 start: { line: cursor.line, ch: 0 },
                 end: {
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index 3429ad7a..da1232c6 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -1,3 +1,4 @@
+import type { Editor, EditorPosition } from 'obsidian';
 import type { Settings } from '../Config/Settings';
 import { DateParser } from '../Query/DateParser';
 import { doAutocomplete } from '../lib/DateAbbreviations';
@@ -496,14 +497,37 @@ export function onlySuggestIfBracketOpen(fn: SuggestionBuilder, brackets: [strin
  * and false value otherwise.
  *
  * This checks for simple pre-conditions:
+ *  - Does the parent editor explicitly request it?
  *  - Is the global filter (if set) in the line?
- *  - Is the line a task line (with a checkbox)
+ *  - Is the line a task line (with a checkbox)?
  * @param line
- * @param cursorPosition - the cursor position, when 0 is presumed to mean 'at the start of the line'.
- *                          See 'ch' in https://docs.obsidian.md/Reference/TypeScript+API/EditorPosition
+ * @param cursor - the cursor position, when ch is 0 it is presumed to mean 'at the start of the line'.
+ *                          See https://docs.obsidian.md/Reference/TypeScript+API/EditorPosition
+ * @param editor - the editor instance to which the suggest belongs
  */
-export function canSuggestForLine(line: string, cursorPosition: number) {
-    return GlobalFilter.getInstance().includedIn(line) && cursorIsInTaskLineDescription(line, cursorPosition);
+export function canSuggestForLine(line: string, cursor: EditorPosition, editor: Editor) {
+    const lineHasGlobalFilter = GlobalFilter.getInstance().includedIn(line);
+    const didEditorRequest = editorIsRequestingSuggest(editor, cursor, lineHasGlobalFilter);
+
+    if (typeof didEditorRequest === 'boolean') return didEditorRequest;
+    return lineHasGlobalFilter && cursorIsInTaskLineDescription(line, cursor.ch);
+}
+
+/**
+ * Return
+ * - true if the parent editor is explicitly requesting that the suggest be displayed
+ * - false if it is requesting that it be hidden
+ * - undefined if the parent editor wants to defer to the default behavior
+ *
+ * @param editor - the editor instance to which the suggest belongs
+ * @param cursor - the cursor position
+ */
+function editorIsRequestingSuggest(
+    editor: Editor,
+    cursor: EditorPosition,
+    lineHasGlobalFilter: boolean,
+): boolean | undefined {
+    return (editor as any)?.editorComponent?.showTasksPluginAutoSuggest?.(cursor, editor, lineHasGlobalFilter);
 }
 
 /**


Test patch:
diff --git a/tests/Suggestor/Suggestor.test.ts b/tests/Suggestor/Suggestor.test.ts
index 82c025fd..5b9f0718 100644
--- a/tests/Suggestor/Suggestor.test.ts
+++ b/tests/Suggestor/Suggestor.test.ts
@@ -239,8 +239,9 @@ describe('canSuggestForLine', () => {
         GlobalFilter.getInstance().reset();
     });
 
-    function canSuggestForLineWithCursor(line: string) {
-        return canSuggestForLine(...cursorPosition(line));
+    function canSuggestForLineWithCursor(line: string, editor: any = {}) {
+        const [testLine, cursorIndex] = cursorPosition(line);
+        return canSuggestForLine(testLine, { line: 0, ch: cursorIndex }, editor);
     }
 
     it('should not suggest if there is no checkbox', () => {
@@ -285,6 +286,24 @@ describe('canSuggestForLine', () => {
         expect(canSuggestForLineWithCursor('    - [ ]|')).toEqual(false);
         expect(canSuggestForLineWithCursor('    - [ ] |')).toEqual(true);
     });
+
+    it('should display when the editor requests the suggest', () => {
+        GlobalFilter.getInstance().reset();
+
+        let shouldShow: boolean | undefined = true;
+        const mockEditor = {
+            editorComponent: {
+                showTasksPluginAutoSuggest: () => shouldShow,
+            },
+        };
+
+        shouldShow = true;
+        expect(canSuggestForLineWithCursor('- not a task line|', mockEditor)).toEqual(true);
+        shouldShow = false;
+        expect(canSuggestForLineWithCursor('- [ ] this *is* a task line|', mockEditor)).toEqual(false);
+        shouldShow = undefined;
+        expect(canSuggestForLineWithCursor('- [ ] this *is* a task line|', mockEditor)).toEqual(true);
+    });
 });
 
 describe('lastOpenBracket', () => {