update and fix
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-sample-plugin",
|
"name": "bindthem",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-sample-plugin",
|
"name": "bindthem",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "0-BSD",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"obsidian": "latest"
|
"obsidian": "latest"
|
||||||
},
|
},
|
||||||
|
|||||||
328
src/SelectionOccurrence.ts
Normal file
328
src/SelectionOccurrence.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import { Editor, EditorPosition, EditorSelection } from 'obsidian'
|
||||||
|
import { selectionToRange } from './Utils'
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Selection occurrence tracking (ported from obsidian-editor-shortcuts)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
Properties used to distinguish between selections that are programmatic
|
||||||
|
(expanding from a cursor selection) vs. manual (using a mouse / Shift + arrow
|
||||||
|
keys). This controls the match behaviour for selectWordOrNextOccurrence.
|
||||||
|
*/
|
||||||
|
let isManualSelection = true
|
||||||
|
let isProgrammaticSelectionChange = false
|
||||||
|
|
||||||
|
export const setIsManualSelection = (value: boolean) => {
|
||||||
|
isManualSelection = value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setIsProgrammaticSelectionChange = (value: boolean) => {
|
||||||
|
isProgrammaticSelectionChange = value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIsManualSelection = () => isManualSelection
|
||||||
|
export const getIsProgrammaticSelectionChange = () => isProgrammaticSelectionChange
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if all selections have the same text content
|
||||||
|
*/
|
||||||
|
function hasSameSelectionContent(
|
||||||
|
editor: Editor,
|
||||||
|
selections: EditorSelection[],
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
new Set(
|
||||||
|
selections.map((selection) => {
|
||||||
|
const { from, to } = selectionToRange(selection)
|
||||||
|
return editor.getRange(from, to)
|
||||||
|
}),
|
||||||
|
).size === 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the search text from the current selections.
|
||||||
|
* If autoExpand is true and no text is selected, expands to word at cursor.
|
||||||
|
*/
|
||||||
|
function getSearchText({
|
||||||
|
editor,
|
||||||
|
allSelections,
|
||||||
|
autoExpand,
|
||||||
|
}: {
|
||||||
|
editor: Editor
|
||||||
|
allSelections: EditorSelection[]
|
||||||
|
autoExpand: boolean
|
||||||
|
}): { searchText: string; singleSearchText: boolean } {
|
||||||
|
const singleSearchText = hasSameSelectionContent(editor, allSelections)
|
||||||
|
const firstSelection = allSelections[0]
|
||||||
|
const { from, to } = selectionToRange(firstSelection)
|
||||||
|
let searchText = editor.getRange(from, to)
|
||||||
|
if (searchText.length === 0 && autoExpand) {
|
||||||
|
const wordRange = editor.wordAt(from)
|
||||||
|
if (wordRange) {
|
||||||
|
searchText = editor.getRange(wordRange.from, wordRange.to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { searchText, singleSearchText }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes any special regex characters in the given string.
|
||||||
|
*/
|
||||||
|
const escapeRegex = (input: string) =>
|
||||||
|
input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a custom regex query with word boundaries because `\b` in JS
|
||||||
|
* doesn't match word boundaries for unicode characters, even with the unicode flag.
|
||||||
|
*
|
||||||
|
* Adapted from https://shiba1014.medium.com/regex-word-boundaries-with-unicode-207794f6e7ed.
|
||||||
|
*/
|
||||||
|
const withWordBoundaries = (input: string) =>
|
||||||
|
`(?<=\\W|^)${input}(?=\\W|$)`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all match positions of searchText in the document content.
|
||||||
|
* When searchWithinWords is false, only whole-word matches are returned.
|
||||||
|
*/
|
||||||
|
function findAllMatches({
|
||||||
|
searchText,
|
||||||
|
searchWithinWords,
|
||||||
|
documentContent,
|
||||||
|
}: {
|
||||||
|
searchText: string
|
||||||
|
searchWithinWords: boolean
|
||||||
|
documentContent: string
|
||||||
|
}): RegExpMatchArray[] {
|
||||||
|
const escapedSearchText = escapeRegex(searchText)
|
||||||
|
const searchExpression = new RegExp(
|
||||||
|
searchWithinWords
|
||||||
|
? escapedSearchText
|
||||||
|
: withWordBoundaries(escapedSearchText),
|
||||||
|
'g',
|
||||||
|
)
|
||||||
|
return Array.from(documentContent.matchAll(searchExpression))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all match positions as editor selections.
|
||||||
|
*/
|
||||||
|
function findAllMatchPositions({
|
||||||
|
editor,
|
||||||
|
searchText,
|
||||||
|
searchWithinWords,
|
||||||
|
documentContent,
|
||||||
|
}: {
|
||||||
|
editor: Editor
|
||||||
|
searchText: string
|
||||||
|
searchWithinWords: boolean
|
||||||
|
documentContent: string
|
||||||
|
}): EditorSelection[] {
|
||||||
|
const matches = findAllMatches({
|
||||||
|
searchText,
|
||||||
|
searchWithinWords,
|
||||||
|
documentContent,
|
||||||
|
})
|
||||||
|
const matchPositions: EditorSelection[] = []
|
||||||
|
for (const match of matches) {
|
||||||
|
matchPositions.push({
|
||||||
|
anchor: editor.offsetToPos(match.index!),
|
||||||
|
head: editor.offsetToPos(match.index! + searchText.length),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return matchPositions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the next match position after a given position.
|
||||||
|
* If no match is found after, wraps around to the beginning.
|
||||||
|
* Already-selected positions are skipped.
|
||||||
|
*/
|
||||||
|
function findNextMatchPosition({
|
||||||
|
editor,
|
||||||
|
latestMatchPos,
|
||||||
|
searchText,
|
||||||
|
searchWithinWords,
|
||||||
|
documentContent,
|
||||||
|
}: {
|
||||||
|
editor: Editor
|
||||||
|
latestMatchPos: EditorPosition
|
||||||
|
searchText: string
|
||||||
|
searchWithinWords: boolean
|
||||||
|
documentContent: string
|
||||||
|
}): EditorSelection | null {
|
||||||
|
const latestMatchOffset = editor.posToOffset(latestMatchPos)
|
||||||
|
const matches = findAllMatches({
|
||||||
|
searchText,
|
||||||
|
searchWithinWords,
|
||||||
|
documentContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find first match after current position
|
||||||
|
for (const match of matches) {
|
||||||
|
if (match.index! > latestMatchOffset) {
|
||||||
|
return {
|
||||||
|
anchor: editor.offsetToPos(match.index!),
|
||||||
|
head: editor.offsetToPos(match.index! + searchText.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circle back to search from the top, skipping already-selected positions
|
||||||
|
const selectionIndexes = editor.listSelections().map((selection) => {
|
||||||
|
const { from } = selectionToRange(selection)
|
||||||
|
return editor.posToOffset(from)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
if (!selectionIndexes.includes(match.index!)) {
|
||||||
|
return {
|
||||||
|
anchor: editor.offsetToPos(match.index!),
|
||||||
|
head: editor.offsetToPos(match.index! + searchText.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the previous match position before a given position.
|
||||||
|
* If no match is found before, wraps around to the end.
|
||||||
|
* Already-selected positions are skipped.
|
||||||
|
*/
|
||||||
|
function findPrevMatchPosition({
|
||||||
|
editor,
|
||||||
|
latestMatchPos,
|
||||||
|
searchText,
|
||||||
|
searchWithinWords,
|
||||||
|
documentContent,
|
||||||
|
}: {
|
||||||
|
editor: Editor
|
||||||
|
latestMatchPos: EditorPosition
|
||||||
|
searchText: string
|
||||||
|
searchWithinWords: boolean
|
||||||
|
documentContent: string
|
||||||
|
}): EditorSelection | null {
|
||||||
|
const latestMatchOffset = editor.posToOffset(latestMatchPos)
|
||||||
|
const matches = findAllMatches({
|
||||||
|
searchText,
|
||||||
|
searchWithinWords,
|
||||||
|
documentContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find last match before current position
|
||||||
|
for (let i = matches.length - 1; i >= 0; i--) {
|
||||||
|
if (matches[i].index! < latestMatchOffset) {
|
||||||
|
return {
|
||||||
|
anchor: editor.offsetToPos(matches[i].index!),
|
||||||
|
head: editor.offsetToPos(matches[i].index! + searchText.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Circle back from the bottom, skipping already-selected positions
|
||||||
|
const selectionIndexes = editor.listSelections().map((selection) => {
|
||||||
|
const { from } = selectionToRange(selection)
|
||||||
|
return editor.posToOffset(from)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = matches.length - 1; i >= 0; i--) {
|
||||||
|
if (!selectionIndexes.includes(matches[i].index!)) {
|
||||||
|
return {
|
||||||
|
anchor: editor.offsetToPos(matches[i].index!),
|
||||||
|
head: editor.offsetToPos(matches[i].index! + searchText.length),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the next or previous occurrence of the current selection text.
|
||||||
|
* When nothing is selected, expands to the word under cursor first.
|
||||||
|
*
|
||||||
|
* Ported from obsidian-editor-shortcuts `selectWordOrNextOccurrence`.
|
||||||
|
*/
|
||||||
|
export function selectWordOrNextOccurrence(
|
||||||
|
editor: Editor,
|
||||||
|
direction: 'next' | 'prev',
|
||||||
|
): void {
|
||||||
|
setIsProgrammaticSelectionChange(true)
|
||||||
|
const allSelections = editor.listSelections()
|
||||||
|
const { searchText, singleSearchText } = getSearchText({
|
||||||
|
editor,
|
||||||
|
allSelections,
|
||||||
|
autoExpand: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (searchText.length > 0 && singleSearchText) {
|
||||||
|
// All selections have the same content — find next/prev occurrence
|
||||||
|
const latestSelection =
|
||||||
|
direction === 'next'
|
||||||
|
? allSelections[allSelections.length - 1]
|
||||||
|
: allSelections[0]
|
||||||
|
const { from: latestMatchPos } = selectionToRange(latestSelection)
|
||||||
|
const findFn =
|
||||||
|
direction === 'next' ? findNextMatchPosition : findPrevMatchPosition
|
||||||
|
const nextMatch = findFn({
|
||||||
|
editor,
|
||||||
|
latestMatchPos,
|
||||||
|
searchText,
|
||||||
|
searchWithinWords: isManualSelection,
|
||||||
|
documentContent: editor.getValue(),
|
||||||
|
})
|
||||||
|
const newSelections = nextMatch
|
||||||
|
? allSelections.concat(nextMatch)
|
||||||
|
: allSelections
|
||||||
|
editor.setSelections(newSelections)
|
||||||
|
const lastSelection = newSelections[newSelections.length - 1]
|
||||||
|
editor.scrollIntoView(selectionToRange(lastSelection))
|
||||||
|
} else {
|
||||||
|
// No text selected — expand each cursor to its word
|
||||||
|
const newSelections: EditorSelection[] = []
|
||||||
|
for (const selection of allSelections) {
|
||||||
|
const { from, to } = selectionToRange(selection)
|
||||||
|
// Don't modify existing range selections
|
||||||
|
if (from.line !== to.line || from.ch !== to.ch) {
|
||||||
|
newSelections.push(selection)
|
||||||
|
} else {
|
||||||
|
const wordAt = editor.wordAt(from)
|
||||||
|
if (wordAt) {
|
||||||
|
newSelections.push({ anchor: wordAt.from, head: wordAt.to })
|
||||||
|
} else {
|
||||||
|
newSelections.push(selection)
|
||||||
|
}
|
||||||
|
setIsManualSelection(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editor.setSelections(newSelections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select all occurrences of the current selection text in the document.
|
||||||
|
* If nothing is selected, expands to the word under cursor first.
|
||||||
|
*
|
||||||
|
* Ported from obsidian-editor-shortcuts `selectAllOccurrences`.
|
||||||
|
*/
|
||||||
|
export function selectAllOccurrences(editor: Editor): void {
|
||||||
|
const allSelections = editor.listSelections()
|
||||||
|
const { searchText, singleSearchText } = getSearchText({
|
||||||
|
editor,
|
||||||
|
allSelections,
|
||||||
|
autoExpand: true,
|
||||||
|
})
|
||||||
|
if (!singleSearchText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const matches = findAllMatchPositions({
|
||||||
|
editor,
|
||||||
|
searchText,
|
||||||
|
searchWithinWords: true,
|
||||||
|
documentContent: editor.getValue(),
|
||||||
|
})
|
||||||
|
editor.setSelections(matches)
|
||||||
|
}
|
||||||
118
src/main.ts
118
src/main.ts
@@ -14,12 +14,18 @@ import { SelectionHelper } from './SelectionHelper';
|
|||||||
import { FileHelper } from './FileHelper';
|
import { FileHelper } from './FileHelper';
|
||||||
import { SentenceNavigator } from './SentenceNavigator';
|
import { SentenceNavigator } from './SentenceNavigator';
|
||||||
import { Direction, Heading, CaseType } from './Entities';
|
import { Direction, Heading, CaseType } from './Entities';
|
||||||
|
import {
|
||||||
|
selectWordOrNextOccurrence,
|
||||||
|
selectAllOccurrences as selectAllOccurrencesFn,
|
||||||
|
setIsManualSelection,
|
||||||
|
setIsProgrammaticSelectionChange,
|
||||||
|
getIsProgrammaticSelectionChange,
|
||||||
|
} from './SelectionOccurrence';
|
||||||
import { DEBUG_HEAD } from './Constants';
|
import { DEBUG_HEAD } from './Constants';
|
||||||
import {
|
import {
|
||||||
getLeadingWhitespace,
|
getLeadingWhitespace,
|
||||||
wordRangeAtPos,
|
wordRangeAtPos,
|
||||||
toTitleCase,
|
toTitleCase,
|
||||||
selectionToRange,
|
|
||||||
} from './Utils';
|
} from './Utils';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -57,6 +63,8 @@ export default class BindThemPlugin extends Plugin {
|
|||||||
// Register commands
|
// Register commands
|
||||||
this.registerCommands();
|
this.registerCommands();
|
||||||
|
|
||||||
|
this.registerSelectionChangeListeners();
|
||||||
|
|
||||||
this.addSettingTab(new BindThemSettingTab(this.app, this));
|
this.addSettingTab(new BindThemSettingTab(this.app, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,14 +633,21 @@ export default class BindThemPlugin extends Plugin {
|
|||||||
name: 'Select Next Occurrence',
|
name: 'Select Next Occurrence',
|
||||||
icon: 'text-cursor',
|
icon: 'text-cursor',
|
||||||
editorCallback: (editor: Editor) => {
|
editorCallback: (editor: Editor) => {
|
||||||
this.selectOccurrence(editor, 'next');
|
selectWordOrNextOccurrence(editor, 'next');
|
||||||
}});
|
}});
|
||||||
this.addConditionalCommand({
|
this.addConditionalCommand({
|
||||||
id: 'select-prev-occurrence',
|
id: 'select-prev-occurrence',
|
||||||
name: 'Select Previous Occurrence',
|
name: 'Select Previous Occurrence',
|
||||||
icon: 'text-cursor',
|
icon: 'text-cursor',
|
||||||
editorCallback: (editor: Editor) => {
|
editorCallback: (editor: Editor) => {
|
||||||
this.selectOccurrence(editor, 'prev');
|
selectWordOrNextOccurrence(editor, 'prev');
|
||||||
|
}});
|
||||||
|
this.addConditionalCommand({
|
||||||
|
id: 'select-all-occurrences',
|
||||||
|
name: 'Select All Occurrences',
|
||||||
|
icon: 'text-cursor-input',
|
||||||
|
editorCallback: (editor: Editor) => {
|
||||||
|
selectAllOccurrencesFn(editor);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -794,64 +809,6 @@ export default class BindThemPlugin extends Plugin {
|
|||||||
editor.setSelections([...editor.listSelections(), ...newSels]);
|
editor.setSelections([...editor.listSelections(), ...newSels]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectOccurrence(editor: Editor, direction: 'next' | 'prev'): void {
|
|
||||||
const selections = editor.listSelections();
|
|
||||||
if (selections.length === 0) return;
|
|
||||||
|
|
||||||
const lastSelection = direction === 'next' ? selections[selections.length - 1] : selections[0];
|
|
||||||
const range = selectionToRange(lastSelection);
|
|
||||||
const selectedText = editor.getRange(range.from, range.to);
|
|
||||||
|
|
||||||
if (!selectedText) {
|
|
||||||
this.selectionHelper.selectWord(editor, null as unknown as MarkdownView);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullText = editor.getValue();
|
|
||||||
const occurrences: { from: number; to: number }[] = [];
|
|
||||||
let searchPos = 0;
|
|
||||||
while (true) {
|
|
||||||
const idx = fullText.indexOf(selectedText, searchPos);
|
|
||||||
if (idx === -1) break;
|
|
||||||
occurrences.push({ from: idx, to: idx + selectedText.length });
|
|
||||||
searchPos = idx + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (occurrences.length <= 1) return;
|
|
||||||
|
|
||||||
const currentOffset = editor.posToOffset(range.from);
|
|
||||||
let targetOccurrence: { from: number; to: number } | null = null;
|
|
||||||
|
|
||||||
if (direction === 'next') {
|
|
||||||
for (const occ of occurrences) {
|
|
||||||
if (occ.from > currentOffset) {
|
|
||||||
targetOccurrence = occ;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!targetOccurrence) {
|
|
||||||
targetOccurrence = occurrences[0];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = occurrences.length - 1; i >= 0; i--) {
|
|
||||||
if (occurrences[i].from < currentOffset) {
|
|
||||||
targetOccurrence = occurrences[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!targetOccurrence) {
|
|
||||||
targetOccurrence = occurrences[occurrences.length - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetOccurrence) {
|
|
||||||
const from = editor.offsetToPos(targetOccurrence.from);
|
|
||||||
const to = editor.offsetToPos(targetOccurrence.to);
|
|
||||||
const newSelections = [...selections, { anchor: from, head: to }];
|
|
||||||
editor.setSelections(newSelections);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private goToHeading(editor: Editor, direction: 'next' | 'prev'): void {
|
private goToHeading(editor: Editor, direction: 'next' | 'prev'): void {
|
||||||
const file = this.app.workspace.getActiveFile();
|
const file = this.app.workspace.getActiveFile();
|
||||||
const cache = file ? this.app.metadataCache.getFileCache(file) : null;
|
const cache = file ? this.app.metadataCache.getFileCache(file) : null;
|
||||||
@@ -934,6 +891,45 @@ export default class BindThemPlugin extends Plugin {
|
|||||||
inputEl.focus();
|
inputEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Selection Change Listeners
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers event listeners on CodeMirror editors to track whether a
|
||||||
|
* selection change was manual (mouse/keyboard) or programmatic.
|
||||||
|
* This controls the match behavior for select next/previous occurrence:
|
||||||
|
* - Manual changes → whole-word matching
|
||||||
|
* - Programmatic changes → within-word matching
|
||||||
|
*/
|
||||||
|
private registerSelectionChangeListeners(): void {
|
||||||
|
this.app.workspace.onLayoutReady(() => {
|
||||||
|
const MODIFIER_KEYS = [
|
||||||
|
'Control', 'Shift', 'Alt', 'Meta', 'CapsLock', 'Fn',
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleSelectionChange = (evt: Event) => {
|
||||||
|
if (
|
||||||
|
evt instanceof KeyboardEvent &&
|
||||||
|
MODIFIER_KEYS.includes(evt.key)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!getIsProgrammaticSelectionChange()) {
|
||||||
|
setIsManualSelection(true)
|
||||||
|
}
|
||||||
|
setIsProgrammaticSelectionChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe CodeMirror 6 editors (new Obsidian editor)
|
||||||
|
document.querySelectorAll('.cm-content').forEach((el) => {
|
||||||
|
this.registerDomEvent(el as HTMLElement, 'keydown', handleSelectionChange)
|
||||||
|
this.registerDomEvent(el as HTMLElement, 'click', handleSelectionChange)
|
||||||
|
this.registerDomEvent(el as HTMLElement, 'dblclick', handleSelectionChange)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Settings
|
// Settings
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ export const COMMANDS: Record<keyof typeof COMMAND_CATEGORIES, CommandDefinition
|
|||||||
{ id: 'insert-cursor-above', name: 'Insert cursor above' },
|
{ id: 'insert-cursor-above', name: 'Insert cursor above' },
|
||||||
{ id: 'insert-cursor-below', name: 'Insert cursor below' },
|
{ id: 'insert-cursor-below', name: 'Insert cursor below' },
|
||||||
{ id: 'select-next-occurrence', name: 'Select next occurrence' },
|
{ id: 'select-next-occurrence', name: 'Select next occurrence' },
|
||||||
{ id: 'select-prev-occurrence', name: 'Select previous occurrence' }
|
{ id: 'select-prev-occurrence', name: 'Select previous occurrence' },
|
||||||
|
{ id: 'select-all-occurrences', name: 'Select all occurrences' }
|
||||||
],
|
],
|
||||||
fileOperations: [
|
fileOperations: [
|
||||||
{ id: 'duplicate-file', name: 'Duplicate file' },
|
{ id: 'duplicate-file', name: 'Duplicate file' },
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"ignoreDeprecations": "5.0",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"inlineSourceMap": true,
|
"inlineSourceMap": true,
|
||||||
"inlineSources": true,
|
"inlineSources": true,
|
||||||
@@ -14,9 +15,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"DOM",
|
"DOM",
|
||||||
"ES5",
|
"ES2020"
|
||||||
"ES6",
|
|
||||||
"ES7"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|||||||
Reference in New Issue
Block a user