|
| 1 | +import { Range, TextEditor } from "vscode"; |
| 2 | +import { |
| 3 | + ContainingScopeModifier, |
| 4 | + EveryScopeModifier, |
| 5 | + SimpleScopeTypeType, |
| 6 | + Target, |
| 7 | +} from "../../../typings/target.types"; |
| 8 | +import { ProcessedTargetsContext } from "../../../typings/Types"; |
| 9 | +import { ModifierStage } from "../../PipelineStages.types"; |
| 10 | +import ScopeTypeTarget from "../../targets/ScopeTypeTarget"; |
| 11 | +import { processSurroundingPair } from "../surroundingPair"; |
| 12 | +import { fitRangeToLineContent } from "./LineStage"; |
| 13 | + |
| 14 | +export default class ItemStage implements ModifierStage { |
| 15 | + constructor(private modifier: ContainingScopeModifier | EveryScopeModifier) {} |
| 16 | + |
| 17 | + run(context: ProcessedTargetsContext, target: Target): Target[] { |
| 18 | + if (this.modifier.type === "everyScope") { |
| 19 | + return this.getEveryTarget(context, target); |
| 20 | + } |
| 21 | + return [this.getSingleTarget(context, target)]; |
| 22 | + } |
| 23 | + |
| 24 | + private getEveryTarget(context: ProcessedTargetsContext, target: Target) { |
| 25 | + const itemInfos = getItemInfos(context, target); |
| 26 | + |
| 27 | + if (itemInfos.length < 1) { |
| 28 | + throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`); |
| 29 | + } |
| 30 | + |
| 31 | + return itemInfos.map((itemInfo) => this.itemInfoToTarget(target, itemInfo)); |
| 32 | + } |
| 33 | + |
| 34 | + private getSingleTarget(context: ProcessedTargetsContext, target: Target) { |
| 35 | + const itemInfos = getItemInfos(context, target); |
| 36 | + |
| 37 | + const itemInfo = |
| 38 | + itemInfos.find((itemInfo) => |
| 39 | + itemInfo.range.intersection(target.contentRange) |
| 40 | + ) ?? |
| 41 | + itemInfos.find((itemInfo) => |
| 42 | + itemInfo.delimiterRange?.intersection(target.contentRange) |
| 43 | + ); |
| 44 | + |
| 45 | + if (itemInfo == null) { |
| 46 | + throw Error(`Couldn't find containing ${this.modifier.scopeType.type}`); |
| 47 | + } |
| 48 | + |
| 49 | + return this.itemInfoToTarget(target, itemInfo); |
| 50 | + } |
| 51 | + |
| 52 | + private itemInfoToTarget(target: Target, itemInfo: ItemInfo) { |
| 53 | + return new ScopeTypeTarget({ |
| 54 | + scopeTypeType: <SimpleScopeTypeType>this.modifier.scopeType.type, |
| 55 | + editor: target.editor, |
| 56 | + isReversed: target.isReversed, |
| 57 | + contentRange: itemInfo.range, |
| 58 | + delimiter, |
| 59 | + leadingDelimiterRange: itemInfo.leadingDelimiterRange, |
| 60 | + trailingDelimiterRange: itemInfo.trailingDelimiterRange, |
| 61 | + }); |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +function getItemInfos(context: ProcessedTargetsContext, target: Target) { |
| 66 | + const collectionRange = getCollectionRange(context, target); |
| 67 | + return tokensToItemInfos(target.editor, collectionRange); |
| 68 | +} |
| 69 | + |
| 70 | +function getCollectionRange(context: ProcessedTargetsContext, target: Target) { |
| 71 | + let pairInfo = getSurroundingPair( |
| 72 | + context, |
| 73 | + target.editor, |
| 74 | + target.contentRange |
| 75 | + ); |
| 76 | + |
| 77 | + while (pairInfo != null) { |
| 78 | + // The selection from the beginning was this pair and we should not go into the interior but instead look in the parent. |
| 79 | + const isNotInterior = |
| 80 | + target.contentRange.isEqual(pairInfo.contentRange) || |
| 81 | + target.contentRange.start.isBeforeOrEqual(pairInfo.boundary[0].start) || |
| 82 | + target.contentRange.end.isAfterOrEqual(pairInfo.boundary[1].end); |
| 83 | + if (!isNotInterior) { |
| 84 | + return pairInfo.interiorRange; |
| 85 | + } |
| 86 | + // Step out of this pair and see if we have a parent |
| 87 | + const position = target.editor.document.positionAt( |
| 88 | + target.editor.document.offsetAt(pairInfo.contentRange.start) - 1 |
| 89 | + ); |
| 90 | + pairInfo = getSurroundingPair( |
| 91 | + context, |
| 92 | + target.editor, |
| 93 | + new Range(position, position) |
| 94 | + ); |
| 95 | + } |
| 96 | + |
| 97 | + // We have not found a pair containing the delimiter. Look at the full line. |
| 98 | + return fitRangeToLineContent(target.editor, target.contentRange); |
| 99 | +} |
| 100 | + |
| 101 | +function tokensToItemInfos( |
| 102 | + editor: TextEditor, |
| 103 | + collectionRange: Range |
| 104 | +): ItemInfo[] { |
| 105 | + const tokens = tokenizeRange(editor, collectionRange); |
| 106 | + const itemInfos: ItemInfo[] = []; |
| 107 | + |
| 108 | + tokens.forEach((token, i) => { |
| 109 | + if (token.type === "delimiter") { |
| 110 | + return; |
| 111 | + } |
| 112 | + const leadingDelimiterRange = (() => { |
| 113 | + if (tokens[i - 2]?.type === "item") { |
| 114 | + return new Range(tokens[i - 2].range.end, token.range.start); |
| 115 | + } |
| 116 | + if (tokens[i - 1]?.type === "delimiter") { |
| 117 | + return new Range(tokens[i - 1].range.start, token.range.start); |
| 118 | + } |
| 119 | + return undefined; |
| 120 | + })(); |
| 121 | + const trailingDelimiterRange = (() => { |
| 122 | + if (tokens[i + 2]?.type === "item") { |
| 123 | + return new Range(token.range.end, tokens[i + 2].range.start); |
| 124 | + } |
| 125 | + if (tokens[i + 1]?.type === "delimiter") { |
| 126 | + return new Range(token.range.end, tokens[i + 1].range.end); |
| 127 | + } |
| 128 | + return undefined; |
| 129 | + })(); |
| 130 | + const delimiterRange = |
| 131 | + tokens[i + 1]?.type === "delimiter" ? tokens[i + 1].range : undefined; |
| 132 | + itemInfos.push({ |
| 133 | + range: token.range, |
| 134 | + leadingDelimiterRange, |
| 135 | + trailingDelimiterRange, |
| 136 | + delimiterRange, |
| 137 | + }); |
| 138 | + }); |
| 139 | + |
| 140 | + return itemInfos; |
| 141 | +} |
| 142 | + |
| 143 | +function tokenizeRange(editor: TextEditor, collectionRange: Range) { |
| 144 | + const { document } = editor; |
| 145 | + const text = document.getText(collectionRange); |
| 146 | + const parts = text.split(/([,(){}<>[\]"'])/g).filter(Boolean); |
| 147 | + const tokens: Token[] = []; |
| 148 | + let offset = document.offsetAt(collectionRange.start); |
| 149 | + let waitingForDelimiter: string | null = null; |
| 150 | + let offsetStart = 0; |
| 151 | + |
| 152 | + parts.forEach((text) => { |
| 153 | + // Whitespace found. Just skip |
| 154 | + if (text.trim().length === 0) { |
| 155 | + offset += text.length; |
| 156 | + return; |
| 157 | + } |
| 158 | + |
| 159 | + // We are waiting for a closing delimiter |
| 160 | + if (waitingForDelimiter != null) { |
| 161 | + // Closing delimiter found |
| 162 | + if (waitingForDelimiter === text) { |
| 163 | + waitingForDelimiter = null; |
| 164 | + tokens.push({ |
| 165 | + type: "item", |
| 166 | + range: new Range( |
| 167 | + document.positionAt(offsetStart), |
| 168 | + document.positionAt(offset + text.length) |
| 169 | + ), |
| 170 | + }); |
| 171 | + } |
| 172 | + } |
| 173 | + // Separator delimiter found. |
| 174 | + else if (text === delimiter) { |
| 175 | + tokens.push({ |
| 176 | + type: "delimiter", |
| 177 | + range: new Range( |
| 178 | + document.positionAt(offset), |
| 179 | + document.positionAt(offset + text.length) |
| 180 | + ), |
| 181 | + }); |
| 182 | + } |
| 183 | + // Starting delimiter found |
| 184 | + else if (delimiters[text] != null) { |
| 185 | + waitingForDelimiter = delimiters[text]; |
| 186 | + offsetStart = offset; |
| 187 | + } |
| 188 | + // Text/item content found |
| 189 | + else { |
| 190 | + const offsetStart = offset + (text.length - text.trimStart().length); |
| 191 | + tokens.push({ |
| 192 | + type: "item", |
| 193 | + range: new Range( |
| 194 | + document.positionAt(offsetStart), |
| 195 | + document.positionAt(offsetStart + text.trim().length) |
| 196 | + ), |
| 197 | + }); |
| 198 | + } |
| 199 | + |
| 200 | + offset += text.length; |
| 201 | + }); |
| 202 | + |
| 203 | + return tokens; |
| 204 | +} |
| 205 | + |
| 206 | +function getSurroundingPair( |
| 207 | + context: ProcessedTargetsContext, |
| 208 | + editor: TextEditor, |
| 209 | + contentRange: Range |
| 210 | +) { |
| 211 | + return processSurroundingPair(context, editor, contentRange, { |
| 212 | + type: "surroundingPair", |
| 213 | + delimiter: "any", |
| 214 | + }); |
| 215 | +} |
| 216 | + |
| 217 | +interface ItemInfo { |
| 218 | + range: Range; |
| 219 | + leadingDelimiterRange?: Range; |
| 220 | + trailingDelimiterRange?: Range; |
| 221 | + delimiterRange?: Range; |
| 222 | +} |
| 223 | + |
| 224 | +interface Token { |
| 225 | + range: Range; |
| 226 | + type: string; |
| 227 | +} |
| 228 | + |
| 229 | +const delimiter = ","; |
| 230 | + |
| 231 | +// Mapping between opening and closing delimiters |
| 232 | +const delimiters: { [key: string]: string } = { |
| 233 | + "(": ")", |
| 234 | + "{": "}", |
| 235 | + "<": ">", |
| 236 | + "[": "]", |
| 237 | + '"': '"', |
| 238 | + "'": "'", |
| 239 | +}; |
0 commit comments