From 277303eacdf5ca19799d6d797eed424f0bd575a2 Mon Sep 17 00:00:00 2001 From: Pokey Rule Date: Tue, 7 Dec 2021 17:27:31 +0000 Subject: [PATCH] Initial working version --- src/languages/constants.ts | 15 ++++ src/languages/getTextFragmentExtractor.ts | 68 +++++++++++++++++++ src/languages/index.ts | 16 ++++- src/languages/json.ts | 21 +++++- .../modifiers/surroundingPair/index.ts | 40 +++-------- .../parseTree/json/clearPair.yml | 23 +++++++ .../parseTree/json/clearPair2.yml | 23 +++++++ .../parseTree/json/clearRound.yml | 23 +++++++ 8 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 src/languages/constants.ts create mode 100644 src/languages/getTextFragmentExtractor.ts create mode 100644 src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair.yml create mode 100644 src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair2.yml create mode 100644 src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearRound.yml diff --git a/src/languages/constants.ts b/src/languages/constants.ts new file mode 100644 index 0000000000..2ead5eb26e --- /dev/null +++ b/src/languages/constants.ts @@ -0,0 +1,15 @@ +export const supportedLanguageIds = [ + "c", + "cpp", + "csharp", + "java", + "javascript", + "javascriptreact", + "json", + "jsonc", + "python", + "typescript", + "typescriptreact", +] as const; + +export type SupportedLanguageId = typeof supportedLanguageIds[number]; diff --git a/src/languages/getTextFragmentExtractor.ts b/src/languages/getTextFragmentExtractor.ts new file mode 100644 index 0000000000..e4fd9389ca --- /dev/null +++ b/src/languages/getTextFragmentExtractor.ts @@ -0,0 +1,68 @@ +import { SyntaxNode } from "web-tree-sitter"; +import { SelectionWithEditor } from "../typings/Types"; +import { textFragmentExtractor as jsonTextFragmentExtractor } from "./json"; +import { UnsupportedLanguageError } from "../errors"; +import { Range } from "vscode"; +import { SupportedLanguageId } from "./constants"; +import { getNodeRange, makeRangeFromPositions } from "../util/nodeSelectors"; +import { getNodeMatcher } from "./index"; + +export type TextFragmentExtractor = ( + node: SyntaxNode, + selection: SelectionWithEditor +) => Range | null; + +function constructDefaultTextFragmentExtractor( + languageId: SupportedLanguageId +): TextFragmentExtractor { + const stringNodeMatcher = getNodeMatcher(languageId, "string", false); + const commentNodeMatcher = getNodeMatcher(languageId, "comment", false); + + return (node: SyntaxNode, selection: SelectionWithEditor) => { + const isStringNode = stringNodeMatcher(selection, node) != null; + + if (isStringNode || commentNodeMatcher(selection, node) != null) { + if (isStringNode) { + const children = node.children; + + return makeRangeFromPositions( + children[0].endPosition, + children[children.length - 1].startPosition + ); + } else { + return getNodeRange(node); + } + } + + return null; + }; +} + +export default function getTextFragmentExtractor( + languageId: string +): TextFragmentExtractor { + const extractor = textFragmentExtractors[languageId as SupportedLanguageId]; + + if (extractor == null) { + throw new UnsupportedLanguageError(languageId); + } + + return extractor; +} + +const textFragmentExtractors: Record< + SupportedLanguageId, + TextFragmentExtractor +> = { + c: constructDefaultTextFragmentExtractor("c"), + cpp: constructDefaultTextFragmentExtractor("cpp"), + csharp: constructDefaultTextFragmentExtractor("csharp"), + java: constructDefaultTextFragmentExtractor("java"), + javascript: constructDefaultTextFragmentExtractor("javascript"), + javascriptreact: constructDefaultTextFragmentExtractor("javascriptreact"), + jsonc: jsonTextFragmentExtractor, + json: jsonTextFragmentExtractor, + python: constructDefaultTextFragmentExtractor("python"), + typescript: constructDefaultTextFragmentExtractor("typescript"), + typescriptreact: constructDefaultTextFragmentExtractor("typescriptreact"), +}; diff --git a/src/languages/index.ts b/src/languages/index.ts index 30458856b6..9b361b19fb 100644 --- a/src/languages/index.ts +++ b/src/languages/index.ts @@ -10,12 +10,16 @@ import { import cpp from "./cpp"; import csharp from "./csharp"; import java from "./java"; -import json from "./json"; +import { patternMatchers as json } from "./json"; import python from "./python"; import typescript from "./typescript"; import { UnsupportedLanguageError } from "../errors"; +import { SupportedLanguageId, supportedLanguageIds } from "./constants"; -const languageMatchers: Record> = { +const languageMatchers: Record< + SupportedLanguageId, + Record +> = { c: cpp, cpp: cpp, csharp: csharp, @@ -29,12 +33,18 @@ const languageMatchers: Record> = { typescriptreact: typescript, }; +export function isLanguageSupported( + languageId: string +): languageId is SupportedLanguageId { + return languageId in supportedLanguageIds; +} + export function getNodeMatcher( languageId: string, scopeType: ScopeType, includeSiblings: boolean ): NodeMatcher { - const matchers = languageMatchers[languageId]; + const matchers = languageMatchers[languageId as SupportedLanguageId]; if (matchers == null) { throw new UnsupportedLanguageError(languageId); diff --git a/src/languages/json.ts b/src/languages/json.ts index 784b31f6fc..5a45e86cc5 100644 --- a/src/languages/json.ts +++ b/src/languages/json.ts @@ -4,7 +4,13 @@ import { leadingMatcher, trailingMatcher, } from "../util/nodeMatchers"; -import { ScopeType, NodeMatcherAlternative } from "../typings/Types"; +import { + ScopeType, + NodeMatcherAlternative, + SelectionWithEditor, +} from "../typings/Types"; +import { SyntaxNode } from "web-tree-sitter"; +import { getNodeRange } from "../util/nodeSelectors"; const nodeMatchers: Partial> = { map: "object", @@ -16,4 +22,15 @@ const nodeMatchers: Partial> = { collectionItem: argumentMatcher("object", "array"), }; -export default createPatternMatchers(nodeMatchers); +export const patternMatchers = createPatternMatchers(nodeMatchers); + +export function textFragmentExtractor( + node: SyntaxNode, + selection: SelectionWithEditor +) { + if (node.type === "string_content") { + return getNodeRange(node); + } + + return null; +} diff --git a/src/processTargets/modifiers/surroundingPair/index.ts b/src/processTargets/modifiers/surroundingPair/index.ts index bf813e3701..a62ae6e360 100644 --- a/src/processTargets/modifiers/surroundingPair/index.ts +++ b/src/processTargets/modifiers/surroundingPair/index.ts @@ -16,6 +16,9 @@ import { } from "../../../util/nodeSelectors"; import { SelectionWithEditorWithContext } from "../processModifier"; import { complexDelimiterMap } from "./delimiterMaps"; +import getTextFragmentExtractor, { + TextFragmentExtractor, +} from "../../../languages/getTextFragmentExtractor"; /** * Applies the surrounding pair modifier to the given selection. First looks to @@ -41,14 +44,14 @@ export function processSurroundingPair( ] ?? [modifier.delimiter]; let node: SyntaxNode | null; - let stringNodeMatcher: NodeMatcher; - let commentNodeMatcher: NodeMatcher; + let textFragmentExtractor: TextFragmentExtractor; + try { node = context.getNodeAtLocation( new Location(document.uri, selection.selection) ); - stringNodeMatcher = getNodeMatcher(document.languageId, "string", false); - commentNodeMatcher = getNodeMatcher(document.languageId, "comment", false); + + textFragmentExtractor = getTextFragmentExtractor(document.languageId); } catch (err) { if ((err as Error).name === "UnsupportedLanguageError") { // If we're in a language where we don't have a parse tree we use the text @@ -68,35 +71,12 @@ export function processSurroundingPair( // If we have a parse tree but we are in a string node or in a comment node, // then we use the text-based algorithm - const isStringNode = stringNodeMatcher(selection, node) != null; - if (isStringNode || commentNodeMatcher(selection, node) != null) { - let nodeRange: Range; - - if (isStringNode) { - const children = node.children; - - if (children.length !== 0) { - nodeRange = makeRangeFromPositions( - children[0].endPosition, - children[children.length - 1].startPosition - ); - } else { - // This is a hack to deal with the fact that java doesn't have - // quotation mark tokens as children of the string. Rather than letting - // the parse tree handle the quotation marks in java, we instead just - // let the textual surround handle them by letting it see the quotation - // marks. In other languages we prefer to let the parser handle the - // quotation marks in case they are more than one character long. - nodeRange = getNodeRange(node); - } - } else { - nodeRange = getNodeRange(node); - } - + const textFragmentRange = textFragmentExtractor(node, selection); + if (textFragmentRange != null) { const surroundingRange = findSurroundingPairTextBased( selection.editor, selection.selection, - nodeRange, + textFragmentRange, delimiters, modifier.delimiterInclusion, modifier.forceDirection diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair.yml new file mode 100644 index 0000000000..fa01055470 --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair.yml @@ -0,0 +1,23 @@ +languageId: json +command: + version: 0 + spokenForm: clear pair + action: clearAndSetSelection + targets: + - type: primitive + modifier: {type: surroundingPair, delimiter: any} +initialState: + documentContents: "\"(hello)\"" + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: "\"\"" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + thatMark: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair2.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair2.yml new file mode 100644 index 0000000000..17d44ab65c --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearPair2.yml @@ -0,0 +1,23 @@ +languageId: json +command: + version: 0 + spokenForm: clear pair + action: clearAndSetSelection + targets: + - type: primitive + modifier: {type: surroundingPair, delimiter: any} +initialState: + documentContents: "\"(hello)\"" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + marks: {} +finalState: + documentContents: "" + selections: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} + thatMark: + - anchor: {line: 0, character: 0} + active: {line: 0, character: 0} +fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: any}}] diff --git a/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearRound.yml b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearRound.yml new file mode 100644 index 0000000000..f9393dd33a --- /dev/null +++ b/src/test/suite/fixtures/recorded/surroundingPair/parseTree/json/clearRound.yml @@ -0,0 +1,23 @@ +languageId: json +command: + version: 0 + spokenForm: clear round + action: clearAndSetSelection + targets: + - type: primitive + modifier: {type: surroundingPair, delimiter: parentheses} +initialState: + documentContents: "\"(hello)\"" + selections: + - anchor: {line: 0, character: 5} + active: {line: 0, character: 5} + marks: {} +finalState: + documentContents: "\"\"" + selections: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} + thatMark: + - anchor: {line: 0, character: 1} + active: {line: 0, character: 1} +fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: surroundingPair, delimiter: parentheses}}]