From c4de04cd1f2fed73f4ab31c21f9a3402ead85d87 Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Sat, 13 Feb 2021 08:48:17 +1100 Subject: [PATCH 01/12] Add 'inString' and 'inComment' contexts - for https://github.com/BetterThanTomorrow/calva/issues/1023 - resolves #1023 --- src/config.ts | 4 ++- src/cursor-doc/token-cursor.ts | 8 +++++ src/extension.ts | 62 ++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 20063a278..13426b70b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,9 @@ const config = { REPL_FILE_EXT: 'calva-repl', KEYBINDINGS_ENABLED_CONFIG_KEY: 'calva.keybindingsEnabled', - KEYBINDINGS_ENABLED_CONTEXT_KEY: 'calva:keybindingsEnabled' + KEYBINDINGS_ENABLED_CONTEXT_KEY: 'calva:keybindingsEnabled', + CURSOR_CONTEXT_IN_STRING: 'calva:inString', + CURSOR_CONTEXT_IN_COMMENT: 'calva:inComment', }; type ReplSessionType = 'clj' | 'cljs'; diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index 0796993ab..3c4356494 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -670,6 +670,14 @@ export class LispTokenCursor extends TokenCursor { return false; } + /** + * Indicates if the current token is in a comment line + */ + withinComment() { + const cursor = this.clone(); + return cursor.getToken().type === 'comment' || cursor.getPrevToken().type === 'comment'; + } + /** * Tells if the cursor is inside a properly closed list. */ diff --git a/src/extension.ts b/src/extension.ts index a1782c3e9..acb81d41a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,6 +30,9 @@ import config from './config'; import handleNewCljFiles from './fileHandler'; import lsp from './lsp'; import * as snippets from './custom-snippets'; +import * as docMirror from './doc-mirror' +import { testDataDir } from './extension-test/integration/suite/util'; +import { commands } from 'fast-check/*'; async function onDidSave(document) { let { @@ -65,6 +68,7 @@ function setKeybindingsEnabledContext() { } async function activate(context: vscode.ExtensionContext) { + vscode.commands.executeCommand('setContext', "@AFARK", "farked"); status.updateNeedReplUi(false, context); lsp.activate(context).then(debugDecorations.triggerUpdateAndRenderDecorations); state.cursor.set('analytics', new Analytics(context)); @@ -219,6 +223,7 @@ async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor((editor) => { status.update(); replHistory.setReplHistoryCommandsActiveContext(editor); + checkForContextChange(editor); })); context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(annotations.onDidChangeTextDocument)); context.subscriptions.push(new vscode.Disposable(() => { @@ -231,6 +236,7 @@ async function activate(context: vscode.ExtensionContext) { })); context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => { replHistory.setReplHistoryCommandsActiveContext(event.textEditor); + checkForContextChange(event.textEditor); })); context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(document => { if (outputWindow.isResultsDoc(document)) { @@ -311,6 +317,62 @@ async function activate(context: vscode.ExtensionContext) { } } +// TODO: remove console.logs +type CursorContext = 'calva-standard' | AltCursorContext; +type AltCursorContext = 'string' | 'comment'; +let lastContext: CursorContext = null; + +function checkForContextChange(editor: vscode.TextEditor) { + if (!editor || !editor.document || editor.document.languageId !== 'clojure') return; + + const currentContext = getCursorContext(editor.document, editor.selection.active); + setNewContext(currentContext); +} + +function setNewContext(currentContext: CursorContext) { + if (lastContext === currentContext) { + return; + } + lastContext = currentContext; + const commentContext = config.CURSOR_CONTEXT_IN_COMMENT; + const stringContext = config.CURSOR_CONTEXT_IN_STRING; + vscode.commands.executeCommand('setContext', commentContext, false); + vscode.commands.executeCommand('setContext', stringContext, false); + + switch (currentContext) { + case 'calva-standard': + console.log(`context: calva-standard`); + break; + case 'comment': + console.log(`context: ${commentContext}`); + vscode.commands.executeCommand('setContext', commentContext, true); + break; + case 'string': + console.log(`context: ${stringContext}`); + vscode.commands.executeCommand('setContext', stringContext, true); + break; + default: + // TODO: throw? log? + } +} + +function getCursorContext(document: vscode.TextDocument, position: vscode.Position): CursorContext { + const idx = document.offsetAt(position); + const mirrorDoc = docMirror.getDocument(document); + const tokenCursor = mirrorDoc.getTokenCursor(idx); + + let context: CursorContext; + if (tokenCursor.withinString()) { + context = 'string'; + } else if (tokenCursor.withinComment()) { + context = 'comment'; + } else { + context = 'calva-standard'; + } + + return context; +} + function deactivate(): Promise | undefined { state.analytics().logEvent("LifeCycle", "Deactivated").send(); jackIn.calvaJackout(); From 9f2100527b0d7cdfe52d637c666ef2a4393a5fe2 Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Sat, 13 Feb 2021 10:58:09 +1100 Subject: [PATCH 02/12] Add to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee30cad39..8ba60f292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Changes to Calva. ## [Unreleased] +- Fix: [Allow user to override keybindings in comments and/or strings](https://github.com/BetterThanTomorrow/calva/issues/1023) ## [2.0.171] - 2021-02-10 - Update clojure-lsp to version 2021.02.09-18.28.06 (Fix: [Auto completion does not work in clojure-lsp only mode (no repl connection)](https://github.com/BetterThanTomorrow/calva/issues/996#issuecomment-776148282)) From 0e480de3577a91594a6003bf1cc694aefa3f2e6b Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Sat, 13 Feb 2021 13:47:52 +1100 Subject: [PATCH 03/12] Minor renamings & excisions --- src/config.ts | 4 ++-- src/extension.ts | 21 +++++++-------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/config.ts b/src/config.ts index 13426b70b..632a4d6ef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,8 +2,8 @@ const config = { REPL_FILE_EXT: 'calva-repl', KEYBINDINGS_ENABLED_CONFIG_KEY: 'calva.keybindingsEnabled', KEYBINDINGS_ENABLED_CONTEXT_KEY: 'calva:keybindingsEnabled', - CURSOR_CONTEXT_IN_STRING: 'calva:inString', - CURSOR_CONTEXT_IN_COMMENT: 'calva:inComment', + CURSOR_CONTEXT_IN_STRING: 'calva:cursorInString', + CURSOR_CONTEXT_IN_COMMENT: 'calva:cursorInComment', }; type ReplSessionType = 'clj' | 'cljs'; diff --git a/src/extension.ts b/src/extension.ts index acb81d41a..8e7d2ca13 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -31,8 +31,6 @@ import handleNewCljFiles from './fileHandler'; import lsp from './lsp'; import * as snippets from './custom-snippets'; import * as docMirror from './doc-mirror' -import { testDataDir } from './extension-test/integration/suite/util'; -import { commands } from 'fast-check/*'; async function onDidSave(document) { let { @@ -68,7 +66,6 @@ function setKeybindingsEnabledContext() { } async function activate(context: vscode.ExtensionContext) { - vscode.commands.executeCommand('setContext', "@AFARK", "farked"); status.updateNeedReplUi(false, context); lsp.activate(context).then(debugDecorations.triggerUpdateAndRenderDecorations); state.cursor.set('analytics', new Analytics(context)); @@ -223,7 +220,7 @@ async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor((editor) => { status.update(); replHistory.setReplHistoryCommandsActiveContext(editor); - checkForContextChange(editor); + setCursorContextIfChanged(editor); })); context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(annotations.onDidChangeTextDocument)); context.subscriptions.push(new vscode.Disposable(() => { @@ -236,7 +233,7 @@ async function activate(context: vscode.ExtensionContext) { })); context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(event => { replHistory.setReplHistoryCommandsActiveContext(event.textEditor); - checkForContextChange(event.textEditor); + setCursorContextIfChanged(event.textEditor); })); context.subscriptions.push(vscode.workspace.onDidCloseTextDocument(document => { if (outputWindow.isResultsDoc(document)) { @@ -317,19 +314,18 @@ async function activate(context: vscode.ExtensionContext) { } } -// TODO: remove console.logs type CursorContext = 'calva-standard' | AltCursorContext; type AltCursorContext = 'string' | 'comment'; let lastContext: CursorContext = null; -function checkForContextChange(editor: vscode.TextEditor) { +function setCursorContextIfChanged(editor: vscode.TextEditor) { if (!editor || !editor.document || editor.document.languageId !== 'clojure') return; - const currentContext = getCursorContext(editor.document, editor.selection.active); - setNewContext(currentContext); + const currentContext = determineCursorContext(editor.document, editor.selection.active); + setCursorContext(currentContext); } -function setNewContext(currentContext: CursorContext) { +function setCursorContext(currentContext: CursorContext) { if (lastContext === currentContext) { return; } @@ -341,14 +337,11 @@ function setNewContext(currentContext: CursorContext) { switch (currentContext) { case 'calva-standard': - console.log(`context: calva-standard`); break; case 'comment': - console.log(`context: ${commentContext}`); vscode.commands.executeCommand('setContext', commentContext, true); break; case 'string': - console.log(`context: ${stringContext}`); vscode.commands.executeCommand('setContext', stringContext, true); break; default: @@ -356,7 +349,7 @@ function setNewContext(currentContext: CursorContext) { } } -function getCursorContext(document: vscode.TextDocument, position: vscode.Position): CursorContext { +function determineCursorContext(document: vscode.TextDocument, position: vscode.Position): CursorContext { const idx = document.offsetAt(position); const mirrorDoc = docMirror.getDocument(document); const tokenCursor = mirrorDoc.getTokenCursor(idx); From 9d874096a7edd297162b5914fb8bb8149c8a2dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Str=C3=B6mberg?= Date: Sat, 13 Feb 2021 08:31:38 +0100 Subject: [PATCH 04/12] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba60f292..522ecc007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Changes to Calva. ## [Unreleased] -- Fix: [Allow user to override keybindings in comments and/or strings](https://github.com/BetterThanTomorrow/calva/issues/1023) +- Fix: [Allow keybindings to target when the cursor is inside a comment or a string](https://github.com/BetterThanTomorrow/calva/issues/1023) ## [2.0.171] - 2021-02-10 - Update clojure-lsp to version 2021.02.09-18.28.06 (Fix: [Auto completion does not work in clojure-lsp only mode (no repl connection)](https://github.com/BetterThanTomorrow/calva/issues/996#issuecomment-776148282)) From bae3efffe7c3fafd60fdf0baa880e0614f9fc491 Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Mon, 15 Feb 2021 21:09:57 +1100 Subject: [PATCH 05/12] Make 'InComment' context explicity include EOL --- src/cursor-doc/token-cursor.ts | 3 ++- src/extension.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index 3c4356494..6013def13 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -675,7 +675,8 @@ export class LispTokenCursor extends TokenCursor { */ withinComment() { const cursor = this.clone(); - return cursor.getToken().type === 'comment' || cursor.getPrevToken().type === 'comment'; + const token_type = cursor.getToken().type; + return token_type === 'comment' || (token_type === 'eol' && cursor.getPrevToken().type === 'comment'); } /** diff --git a/src/extension.ts b/src/extension.ts index 8e7d2ca13..22d799908 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -361,7 +361,7 @@ function determineCursorContext(document: vscode.TextDocument, position: vscode. context = 'comment'; } else { context = 'calva-standard'; - } +ma } return context; } From 477a54625cf98bcc0f64b2da0b7c9d4614bc42aa Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Mon, 15 Feb 2021 21:28:23 +1100 Subject: [PATCH 06/12] Move when-context handling to its own file --- src/extension.ts | 54 +------------------------------------------ src/when-contexts.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 src/when-contexts.ts diff --git a/src/extension.ts b/src/extension.ts index 22d799908..1993c18aa 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -30,7 +30,7 @@ import config from './config'; import handleNewCljFiles from './fileHandler'; import lsp from './lsp'; import * as snippets from './custom-snippets'; -import * as docMirror from './doc-mirror' +import setCursorContextIfChanged from './when-contexts' async function onDidSave(document) { let { @@ -314,58 +314,6 @@ async function activate(context: vscode.ExtensionContext) { } } -type CursorContext = 'calva-standard' | AltCursorContext; -type AltCursorContext = 'string' | 'comment'; -let lastContext: CursorContext = null; - -function setCursorContextIfChanged(editor: vscode.TextEditor) { - if (!editor || !editor.document || editor.document.languageId !== 'clojure') return; - - const currentContext = determineCursorContext(editor.document, editor.selection.active); - setCursorContext(currentContext); -} - -function setCursorContext(currentContext: CursorContext) { - if (lastContext === currentContext) { - return; - } - lastContext = currentContext; - const commentContext = config.CURSOR_CONTEXT_IN_COMMENT; - const stringContext = config.CURSOR_CONTEXT_IN_STRING; - vscode.commands.executeCommand('setContext', commentContext, false); - vscode.commands.executeCommand('setContext', stringContext, false); - - switch (currentContext) { - case 'calva-standard': - break; - case 'comment': - vscode.commands.executeCommand('setContext', commentContext, true); - break; - case 'string': - vscode.commands.executeCommand('setContext', stringContext, true); - break; - default: - // TODO: throw? log? - } -} - -function determineCursorContext(document: vscode.TextDocument, position: vscode.Position): CursorContext { - const idx = document.offsetAt(position); - const mirrorDoc = docMirror.getDocument(document); - const tokenCursor = mirrorDoc.getTokenCursor(idx); - - let context: CursorContext; - if (tokenCursor.withinString()) { - context = 'string'; - } else if (tokenCursor.withinComment()) { - context = 'comment'; - } else { - context = 'calva-standard'; -ma } - - return context; -} - function deactivate(): Promise | undefined { state.analytics().logEvent("LifeCycle", "Deactivated").send(); jackIn.calvaJackout(); diff --git a/src/when-contexts.ts b/src/when-contexts.ts new file mode 100644 index 000000000..8e515b006 --- /dev/null +++ b/src/when-contexts.ts @@ -0,0 +1,55 @@ +import * as vscode from 'vscode'; +import config from './config'; +import * as docMirror from './doc-mirror' + +type CursorContext = 'calva-standard' | AltCursorContext; +type AltCursorContext = 'string' | 'comment'; +let lastContext: CursorContext = null; + +export default function setCursorContextIfChanged(editor: vscode.TextEditor) { + if (!editor || !editor.document || editor.document.languageId !== 'clojure') return; + + const currentContext = determineCursorContext(editor.document, editor.selection.active); + setCursorContext(currentContext); +} + +function setCursorContext(currentContext: CursorContext) { + if (lastContext === currentContext) { + return; + } + lastContext = currentContext; + const commentContext = config.CURSOR_CONTEXT_IN_COMMENT; + const stringContext = config.CURSOR_CONTEXT_IN_STRING; + vscode.commands.executeCommand('setContext', commentContext, false); + vscode.commands.executeCommand('setContext', stringContext, false); + + switch (currentContext) { + case 'calva-standard': + break; + case 'comment': + vscode.commands.executeCommand('setContext', commentContext, true); + break; + case 'string': + vscode.commands.executeCommand('setContext', stringContext, true); + break; + default: + // TODO: throw? log? + } +} + +function determineCursorContext(document: vscode.TextDocument, position: vscode.Position): CursorContext { + const idx = document.offsetAt(position); + const mirrorDoc = docMirror.getDocument(document); + const tokenCursor = mirrorDoc.getTokenCursor(idx); + + let context: CursorContext; + if (tokenCursor.withinString()) { + context = 'string'; + } else if (tokenCursor.withinComment()) { + context = 'comment'; + } else { + context = 'calva-standard'; + } + + return context; +} \ No newline at end of file From 0b8d789d7d6d7c87aedd90171be975e2abaf11cd Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Mon, 15 Feb 2021 21:45:05 +1100 Subject: [PATCH 07/12] Move within-comment check out of TokenCursor --- src/config.ts | 4 +--- src/when-contexts.ts | 14 ++++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/config.ts b/src/config.ts index 632a4d6ef..20063a278 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,7 @@ const config = { REPL_FILE_EXT: 'calva-repl', KEYBINDINGS_ENABLED_CONFIG_KEY: 'calva.keybindingsEnabled', - KEYBINDINGS_ENABLED_CONTEXT_KEY: 'calva:keybindingsEnabled', - CURSOR_CONTEXT_IN_STRING: 'calva:cursorInString', - CURSOR_CONTEXT_IN_COMMENT: 'calva:cursorInComment', + KEYBINDINGS_ENABLED_CONTEXT_KEY: 'calva:keybindingsEnabled' }; type ReplSessionType = 'clj' | 'cljs'; diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 8e515b006..da0ae5115 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -1,9 +1,11 @@ import * as vscode from 'vscode'; -import config from './config'; +import { TokenCursor } from './cursor-doc/token-cursor'; import * as docMirror from './doc-mirror' type CursorContext = 'calva-standard' | AltCursorContext; type AltCursorContext = 'string' | 'comment'; +const stringContext = 'calva:cursorInString'; +const commentContext = 'calva:cursorInComment'; let lastContext: CursorContext = null; export default function setCursorContextIfChanged(editor: vscode.TextEditor) { @@ -18,8 +20,7 @@ function setCursorContext(currentContext: CursorContext) { return; } lastContext = currentContext; - const commentContext = config.CURSOR_CONTEXT_IN_COMMENT; - const stringContext = config.CURSOR_CONTEXT_IN_STRING; + vscode.commands.executeCommand('setContext', commentContext, false); vscode.commands.executeCommand('setContext', stringContext, false); @@ -45,11 +46,16 @@ function determineCursorContext(document: vscode.TextDocument, position: vscode. let context: CursorContext; if (tokenCursor.withinString()) { context = 'string'; - } else if (tokenCursor.withinComment()) { + } else if (tokenCursorWithinComment(tokenCursor)) { context = 'comment'; } else { context = 'calva-standard'; } return context; +} + +function tokenCursorWithinComment(cursor: TokenCursor):boolean { + const token_type = cursor.getToken().type; + return token_type === 'comment' || (token_type === 'eol' && cursor.getPrevToken().type === 'comment'); } \ No newline at end of file From 2172add12f607ce0c493442f481a0198b5c6f689 Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Fri, 19 Feb 2021 10:10:42 +1100 Subject: [PATCH 08/12] Minor refactorings --- src/when-contexts.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/when-contexts.ts b/src/when-contexts.ts index da0ae5115..86cb2c41e 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -4,8 +4,8 @@ import * as docMirror from './doc-mirror' type CursorContext = 'calva-standard' | AltCursorContext; type AltCursorContext = 'string' | 'comment'; -const stringContext = 'calva:cursorInString'; -const commentContext = 'calva:cursorInComment'; +const STRING_CONTEXT = 'calva:cursorInString'; +const COMMENT_CONTEXT = 'calva:cursorInComment'; let lastContext: CursorContext = null; export default function setCursorContextIfChanged(editor: vscode.TextEditor) { @@ -21,20 +21,20 @@ function setCursorContext(currentContext: CursorContext) { } lastContext = currentContext; - vscode.commands.executeCommand('setContext', commentContext, false); - vscode.commands.executeCommand('setContext', stringContext, false); + vscode.commands.executeCommand('setContext', COMMENT_CONTEXT, false); + vscode.commands.executeCommand('setContext', STRING_CONTEXT, false); switch (currentContext) { case 'calva-standard': break; case 'comment': - vscode.commands.executeCommand('setContext', commentContext, true); + vscode.commands.executeCommand('setContext', COMMENT_CONTEXT, true); break; case 'string': - vscode.commands.executeCommand('setContext', stringContext, true); + vscode.commands.executeCommand('setContext', STRING_CONTEXT, true); break; default: - // TODO: throw? log? + const checkExhaustive: never = currentContext; } } @@ -46,7 +46,7 @@ function determineCursorContext(document: vscode.TextDocument, position: vscode. let context: CursorContext; if (tokenCursor.withinString()) { context = 'string'; - } else if (tokenCursorWithinComment(tokenCursor)) { + } else if (tokenCursor.withinComment()) { context = 'comment'; } else { context = 'calva-standard'; @@ -54,8 +54,3 @@ function determineCursorContext(document: vscode.TextDocument, position: vscode. return context; } - -function tokenCursorWithinComment(cursor: TokenCursor):boolean { - const token_type = cursor.getToken().type; - return token_type === 'comment' || (token_type === 'eol' && cursor.getPrevToken().type === 'comment'); -} \ No newline at end of file From be7e49ac70781b613cc884fbb57ee019c1a7bdeb Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Sun, 21 Feb 2021 21:20:45 +1100 Subject: [PATCH 09/12] Add calva:cursorAtStart/EndOfLine contexts --- src/cursor-doc/model.ts | 3 +- src/cursor-doc/token-cursor.ts | 2 +- src/when-contexts.ts | 68 +++++++++++++++++----------------- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 38be50533..634f3a0a3 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -8,7 +8,8 @@ export function initScanner(maxLength: number) { } /** A cheesy deep-equal function for matching scanner states. Good enough to compare plain old js objects. */ -function equal(x: any, y: any): boolean { +// TODO: OK to export from here or move to util? +export function equal(x: any, y: any): boolean { if(x==y) return true; if(x instanceof Array && y instanceof Array) { if(x.length == y.length) { diff --git a/src/cursor-doc/token-cursor.ts b/src/cursor-doc/token-cursor.ts index 6013def13..4115384aa 100644 --- a/src/cursor-doc/token-cursor.ts +++ b/src/cursor-doc/token-cursor.ts @@ -676,7 +676,7 @@ export class LispTokenCursor extends TokenCursor { withinComment() { const cursor = this.clone(); const token_type = cursor.getToken().type; - return token_type === 'comment' || (token_type === 'eol' && cursor.getPrevToken().type === 'comment'); + return token_type === 'comment'; } /** diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 86cb2c41e..b7ee19823 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -1,56 +1,56 @@ import * as vscode from 'vscode'; -import { TokenCursor } from './cursor-doc/token-cursor'; +import { Token } from './cursor-doc/lexer'; +import { equal as deep_equal } from './cursor-doc/model' +import { TokenCursor, LispTokenCursor } from './cursor-doc/token-cursor'; import * as docMirror from './doc-mirror' -type CursorContext = 'calva-standard' | AltCursorContext; -type AltCursorContext = 'string' | 'comment'; -const STRING_CONTEXT = 'calva:cursorInString'; -const COMMENT_CONTEXT = 'calva:cursorInComment'; -let lastContext: CursorContext = null; +type CursorContext = LexerCursorContext | PreLexerCursorContext; +type LexerCursorContext = 'calva:cursorInString' | 'calva:cursorInComment'; +type PreLexerCursorContext = 'calva:cursorAtStartOfLine' | 'calva:cursorAtEndOfLine' +const allCursorContexts: CursorContext[] = ['calva:cursorInString', 'calva:cursorInComment', 'calva:cursorAtStartOfLine', 'calva:cursorAtEndOfLine'] + +let lastContexts: CursorContext[] = []; export default function setCursorContextIfChanged(editor: vscode.TextEditor) { if (!editor || !editor.document || editor.document.languageId !== 'clojure') return; - const currentContext = determineCursorContext(editor.document, editor.selection.active); - setCursorContext(currentContext); + const currentContexts = determineCursorContexts(editor.document, editor.selection.active); + setCursorContexts(currentContexts); } -function setCursorContext(currentContext: CursorContext) { - if (lastContext === currentContext) { +function setCursorContexts(currentContexts: CursorContext[]) { + if (deep_equal(lastContexts, currentContexts)) { return; } - lastContext = currentContext; - - vscode.commands.executeCommand('setContext', COMMENT_CONTEXT, false); - vscode.commands.executeCommand('setContext', STRING_CONTEXT, false); - - switch (currentContext) { - case 'calva-standard': - break; - case 'comment': - vscode.commands.executeCommand('setContext', COMMENT_CONTEXT, true); - break; - case 'string': - vscode.commands.executeCommand('setContext', STRING_CONTEXT, true); - break; - default: - const checkExhaustive: never = currentContext; - } + lastContexts = currentContexts; + + allCursorContexts.forEach(context => { + vscode.commands.executeCommand('setContext', context, currentContexts.indexOf(context) > -1) + }) } -function determineCursorContext(document: vscode.TextDocument, position: vscode.Position): CursorContext { +function determineCursorContexts(document: vscode.TextDocument, position: vscode.Position): CursorContext[] { + let contexts: CursorContext[] = []; const idx = document.offsetAt(position); const mirrorDoc = docMirror.getDocument(document); const tokenCursor = mirrorDoc.getTokenCursor(idx); + const line = document.lineAt(position); + + if (position.character <= line.firstNonWhitespaceCharacterIndex) { + contexts.push('calva:cursorAtStartOfLine'); + // no line end within multiline strings + } else if (!tokenCursor.withinString()) { + const lastNonWSIndex = line.text.match(/\S(?=(\s*$))/)?.index; + if (position.character > lastNonWSIndex) { + contexts.push('calva:cursorAtEndOfLine'); + } + } - let context: CursorContext; if (tokenCursor.withinString()) { - context = 'string'; + contexts.push('calva:cursorInString'); } else if (tokenCursor.withinComment()) { - context = 'comment'; - } else { - context = 'calva-standard'; + contexts.push('calva:cursorInComment'); } - return context; + return contexts; } From eec632c7d435d9b2e188106a9be5f073ba4eb97d Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Sun, 21 Feb 2021 21:29:20 +1100 Subject: [PATCH 10/12] Account for clojure commas; refactor --- package-lock.json | 21 +++++----- src/cursor-doc/model.ts | 25 +----------- src/cursor-doc/token-cursor.ts | 2 +- src/util/object.ts | 26 +++++++++++++ src/when-contexts.ts | 71 ++++++++++++++++++++++------------ 5 files changed, 87 insertions(+), 58 deletions(-) create mode 100644 src/util/object.ts diff --git a/package-lock.json b/package-lock.json index d193ed4b7..58499d606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15371,9 +15371,6 @@ }, "bin": { "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" } }, "node_modules/tough-cookie": { @@ -16234,12 +16231,18 @@ "dependencies": { "semver": "^6.3.0", "vscode-languageserver-protocol": "^3.15.3" + }, + "engines": { + "vscode": "^1.41.0" } }, "node_modules/vscode-languageclient/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } }, "node_modules/vscode-languageserver-protocol": { "version": "3.15.3", @@ -16253,7 +16256,10 @@ "node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz", - "integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==" + "integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } }, "node_modules/vscode-languageserver-types": { "version": "3.15.1", @@ -16449,11 +16455,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, - "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], "dependencies": { "is-extendable": "^0.1.0" }, @@ -16466,6 +16468,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" diff --git a/src/cursor-doc/model.ts b/src/cursor-doc/model.ts index 634f3a0a3..bdab1a7b9 100644 --- a/src/cursor-doc/model.ts +++ b/src/cursor-doc/model.ts @@ -1,5 +1,6 @@ import { Scanner, Token, ScannerState } from "./clojure-lexer"; import { LispTokenCursor } from "./token-cursor"; +import { deepEqual as equal} from "../util/object" let scanner: Scanner; @@ -7,30 +8,6 @@ export function initScanner(maxLength: number) { scanner = new Scanner(maxLength); } -/** A cheesy deep-equal function for matching scanner states. Good enough to compare plain old js objects. */ -// TODO: OK to export from here or move to util? -export function equal(x: any, y: any): boolean { - if(x==y) return true; - if(x instanceof Array && y instanceof Array) { - if(x.length == y.length) { - for(let i = 0; i lastNonWSIndex) { - contexts.push('calva:cursorAtEndOfLine'); +// context predicates +function cursorAtLineStartIncLeadingWhitespace(cursor: TokenCursor, documentOffset: number) { + const tk = cursor.clone(); + let startOfLine = false; + // only at start if we're in ws, or at the 1st char of a non-ws sexp + if (tk.getToken().type === 'ws' || tk.offsetStart >= documentOffset) { + while (tk.getPrevToken().type === 'ws') { + tk.previous(); } + startOfLine = tk.getPrevToken().type === 'eol'; } + return startOfLine; +} + +function cursorAtLineEndIncTrailingWhitespace(tokenCursor: LispTokenCursor, line: vscode.TextLine, position: vscode.Position) { + // consider a multiline string as a single line if (tokenCursor.withinString()) { - contexts.push('calva:cursorInString'); - } else if (tokenCursor.withinComment()) { - contexts.push('calva:cursorInComment'); + return false; } - return contexts; + const lastNonWSIndex = line.text.match(/\S(?=(\s*$))/)?.index; + + return position.character > lastNonWSIndex } + From 02ceb3f21a679332b42b0ed6ae745a37fde07b03 Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Sat, 6 Mar 2021 14:10:09 +1100 Subject: [PATCH 11/12] Add POC compound context - some keybinds might require conditions like (calva:cursorInString && !calva:CursorAtEndOfLine), but VSCode API currently doesn't allow parentheses in where clauses (https://github.com/microsoft/vscode/issues/91473) - so add preconfigured compounds where convenient --- src/when-contexts.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 59a2054a1..390ae00f7 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -3,11 +3,12 @@ import { deepEqual } from './util/object' import { LispTokenCursor, TokenCursor } from './cursor-doc/token-cursor'; import * as docMirror from './doc-mirror' -// TODO: what to do about parens not being available in when expressions? // TODO: unit tests -type CursorContext = 'calva:cursorInString' | 'calva:cursorInComment' | 'calva:cursorAtStartOfLine' | 'calva:cursorAtEndOfLine'; -const allCursorContexts: CursorContext[] = ['calva:cursorInString', 'calva:cursorInComment', 'calva:cursorAtStartOfLine', 'calva:cursorAtEndOfLine'] +type CursorContext = CursorAtomicContext | CursorCompoundContext; +type CursorAtomicContext = 'calva:cursorInString' | 'calva:cursorInComment' | 'calva:cursorAtStartOfLine' | 'calva:cursorAtEndOfLine'; +type CursorCompoundContext = 'calva:cursorInsideComment' | 'calva:cursorInCommentExcludingSOL' | 'calva:cursorInCommentExcludingEOL'; +const allCursorContexts: CursorContext[] = ['calva:cursorInString', 'calva:cursorInComment', 'calva:cursorAtStartOfLine', 'calva:cursorAtEndOfLine', 'calva:cursorInsideComment', 'calva:cursorInCommentExcludingSOL', 'calva:cursorInCommentExcludingEOL']; let lastContexts: CursorContext[] = []; @@ -36,6 +37,16 @@ function determineCursorContexts(document: vscode.TextDocument, position: vscode contexts.push('calva:cursorInComment'); } + // Compound contexts + if (contexts.includes('calva:cursorInComment')){ + if (!contexts.includes('calva:cursorAtEndOfLine')){ + contexts.push('calva:cursorInCommentExcludingEOL') + } + if(!contexts.includes('calva:cursorAtStartOfLine')){ + contexts.push('calva:cursorInCommentExcludingSOL') + } + } + return contexts; } @@ -52,6 +63,7 @@ function setCursorContexts(currentContexts: CursorContext[]) { // context predicates +// TODO: make preds for token cursor borrowings (withinstring & wihtincomment) function cursorAtLineStartIncLeadingWhitespace(cursor: TokenCursor, documentOffset: number) { const tk = cursor.clone(); let startOfLine = false; From 136596cb9c1a571d63f7d9aa4f5124de74896cd6 Mon Sep 17 00:00:00 2001 From: Crispin Bennett Date: Sat, 6 Mar 2021 18:06:39 +1100 Subject: [PATCH 12/12] Refactor context predicates for unit testing; --- src/cursor-doc/context-predicates.ts | 44 ++++++++++++++++ .../unit/cursor-doc/context-predicate-test.ts | 52 +++++++++++++++++++ src/when-contexts.ts | 37 ++----------- 3 files changed, 99 insertions(+), 34 deletions(-) create mode 100644 src/cursor-doc/context-predicates.ts create mode 100644 src/extension-test/unit/cursor-doc/context-predicate-test.ts diff --git a/src/cursor-doc/context-predicates.ts b/src/cursor-doc/context-predicates.ts new file mode 100644 index 000000000..352558892 --- /dev/null +++ b/src/cursor-doc/context-predicates.ts @@ -0,0 +1,44 @@ +import { LispTokenCursor } from "./token-cursor"; + +// context predicates + +// TODO: give functions a more similar arg list + +/** + * Returns true if documentOffset is either at the first char of the token under the cursor, or + * in the whitespace between the token and the first preceding EOL, otherwise false + */ +function cursorAtLineStartIncLeadingWhitespace(cursor: LispTokenCursor, documentOffset: number) { + const cursorCopy = cursor.clone(); + let startOfLine = false; + // only at start if we're in ws, or at the 1st char of a non-ws sexp + if (cursorCopy.getToken().type === 'ws' || cursorCopy.offsetStart >= documentOffset) { + while (cursorCopy.getPrevToken().type === 'ws') { + cursorCopy.previous(); + } + startOfLine = cursorCopy.getPrevToken().type === 'eol'; + } + + return startOfLine; +} + +/** + * Returns true if position is after the last char of the last lisp token on the line, including + * any trailing whitespace or EOL, otherwise false + */ +function cursorAtLineEndIncTrailingWhitespace(tokenCursor: LispTokenCursor, line: string, position: number) { + // consider a multiline string as a single line + if (tokenCursor.withinString()) { + return false; + } + + // don't consider commas as whitepace on comment lines + const lastNonWSIndex = line.match(/\S(?=(\s*$))/)?.index; + + return position > lastNonWSIndex +} + +export { + cursorAtLineStartIncLeadingWhitespace, + cursorAtLineEndIncTrailingWhitespace +} \ No newline at end of file diff --git a/src/extension-test/unit/cursor-doc/context-predicate-test.ts b/src/extension-test/unit/cursor-doc/context-predicate-test.ts new file mode 100644 index 000000000..98ae44e9f --- /dev/null +++ b/src/extension-test/unit/cursor-doc/context-predicate-test.ts @@ -0,0 +1,52 @@ +import * as expect from 'expect'; +import * as context from '../../../cursor-doc/context-predicates'; +import { LispTokenCursor } from '../../../cursor-doc/token-cursor'; +import * as mock from '../common/mock'; + +describe('Context Predicate', () => { + const commentSansWs = ';; a comment\n'; + const commentWithLeadingWs = " ;; a comment\n" + const commentWithLeadingAndTrailingWs = " ;; a comment \n" + const docText = `${commentSansWs}${commentWithLeadingWs}${commentWithLeadingAndTrailingWs}`; + let doc: mock.MockDocument; + + beforeEach(() => { + doc = new mock.MockDocument(); + doc.insertString(docText); + }); + + describe('cursorAtLineStartIncLeadingWhitespace', () => { + it('should return true at the start of a line', () => { + const cursor: LispTokenCursor = doc.getTokenCursor(0); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, 0)).toBe(true); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, 1)).toBe(false); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, docText.length - 1)).toBe(false); + }) + + it('should return false within and at the end a line', () => { + const cursor: LispTokenCursor = doc.getTokenCursor(0); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, 1)).toBe(false); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, docText.length - 1)).toBe(false); + }) + + it('should return true at line start with preceding whitespace', () => { + const cursor = doc.getTokenCursor(commentSansWs.length); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, commentSansWs.length)).toBe(true); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, commentSansWs.length + 2)).toBe(true); + }) + + it('should return false within a line with preceding whitespace', () => { + const cursor = doc.getTokenCursor(commentSansWs.length + 2); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, commentSansWs.length + 3)).toBe(false); + }) + + it('should return true at line start with preceding & trailing whitespace', () => { + const line3Offset = commentSansWs.length + commentWithLeadingWs.length; + const cursor = doc.getTokenCursor(line3Offset + 2); + expect(context.cursorAtLineStartIncLeadingWhitespace(cursor, line3Offset + 2)).toBe(true); + }) + }); + + + +}); diff --git a/src/when-contexts.ts b/src/when-contexts.ts index 390ae00f7..a44dc4100 100644 --- a/src/when-contexts.ts +++ b/src/when-contexts.ts @@ -1,9 +1,7 @@ import * as vscode from 'vscode'; import { deepEqual } from './util/object' -import { LispTokenCursor, TokenCursor } from './cursor-doc/token-cursor'; import * as docMirror from './doc-mirror' - -// TODO: unit tests +import * as context from './cursor-doc/context-predicates'; type CursorContext = CursorAtomicContext | CursorCompoundContext; type CursorAtomicContext = 'calva:cursorInString' | 'calva:cursorInComment' | 'calva:cursorAtStartOfLine' | 'calva:cursorAtEndOfLine'; @@ -25,9 +23,9 @@ function determineCursorContexts(document: vscode.TextDocument, position: vscode const mirrorDoc = docMirror.getDocument(document); const tokenCursor = mirrorDoc.getTokenCursor(idx); - if (cursorAtLineStartIncLeadingWhitespace(tokenCursor, document.offsetAt(position))) { + if (context.cursorAtLineStartIncLeadingWhitespace(tokenCursor, document.offsetAt(position))) { contexts.push('calva:cursorAtStartOfLine'); - } else if (cursorAtLineEndIncTrailingWhitespace(tokenCursor, document.lineAt(position), position)) { + } else if (context.cursorAtLineEndIncTrailingWhitespace(tokenCursor, document.lineAt(position).text, position.character)) { contexts.push('calva:cursorAtEndOfLine'); } @@ -60,32 +58,3 @@ function setCursorContexts(currentContexts: CursorContext[]) { vscode.commands.executeCommand('setContext', context, currentContexts.indexOf(context) > -1) }) } - - -// context predicates -// TODO: make preds for token cursor borrowings (withinstring & wihtincomment) -function cursorAtLineStartIncLeadingWhitespace(cursor: TokenCursor, documentOffset: number) { - const tk = cursor.clone(); - let startOfLine = false; - // only at start if we're in ws, or at the 1st char of a non-ws sexp - if (tk.getToken().type === 'ws' || tk.offsetStart >= documentOffset) { - while (tk.getPrevToken().type === 'ws') { - tk.previous(); - } - startOfLine = tk.getPrevToken().type === 'eol'; - } - - return startOfLine; -} - -function cursorAtLineEndIncTrailingWhitespace(tokenCursor: LispTokenCursor, line: vscode.TextLine, position: vscode.Position) { - // consider a multiline string as a single line - if (tokenCursor.withinString()) { - return false; - } - - const lastNonWSIndex = line.text.match(/\S(?=(\s*$))/)?.index; - - return position.character > lastNonWSIndex -} -