blob: 0ce8209ad94132aeee7c5f6aea204db5aab78323 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
import * as vscode from 'vscode'
export function activate(context: vscode.ExtensionContext) {
var controller = new PositionController()
context.subscriptions.push(controller)
context.subscriptions.push(
vscode.commands.registerCommand('position.goto', () =>
controller.goToPositionCommand()
)
)
}
export function deactivate() {
/* nothing to do */
}
function getOptionalNumber(
numberAsString?: string
): [number | undefined, boolean] {
let optionalNumber: number | undefined = undefined
let isRelative: boolean = false
let num: number = Number(numberAsString)
if (!isNaN(num) && numberAsString && numberAsString.length) {
optionalNumber = num
isRelative = ['+', '-'].includes(numberAsString!.charAt(0))
}
return [optionalNumber, isRelative]
}
class PositionController {
private disposable: vscode.Disposable
private statusBarItem: vscode.StatusBarItem
constructor() {
this.statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Right,
100
)
this.statusBarItem.command = 'position.goto'
this.statusBarItem.tooltip = 'Go to position'
// subscribe to selection change and editor activation events
let subscriptions: vscode.Disposable[] = []
vscode.window.onDidChangeTextEditorSelection(
this.onEvent,
this,
subscriptions
)
vscode.window.onDidChangeActiveTextEditor(this.onEvent, this, subscriptions)
// create a combined disposable from both event subscriptions
this.disposable = vscode.Disposable.from(...subscriptions)
this.updatePosition()
}
public goToPositionCommand(): void {
// declaring manager? as optional makes .then/async blocks 'forget' the
// definite assignment/null check inference done after create() in tslint.
let manager: CursorManager
manager = CursorManager.create()!
if (!manager) {
return
}
vscode.window
.showInputBox({
prompt: `Type an offset number from 0 to ${manager.maxPosition}.`,
value: String(manager.cursorOffset),
validateInput: (input: string) => {
manager.previewCursorOffset(input)
return undefined
},
})
.then((input?: string) => {
input !== undefined ? manager.commit() : manager.abort()
})
}
private updatePosition(): void {
let manager = CursorManager.create()
if (!manager) {
this.statusBarItem.hide()
return
}
let offset = manager.cursorOffset
if (offset !== undefined) {
// Update the status bar
this.statusBarItem.text = `Pos ${offset}`
this.statusBarItem.show()
}
}
private onEvent(): void {
this.updatePosition()
}
public dispose() {
this.disposable.dispose()
this.statusBarItem.dispose()
}
}
class CursorManager {
private static cursorPositionDecoration =
vscode.window.createTextEditorDecorationType({
borderColor: new vscode.ThemeColor('editor.foreground'),
borderStyle: 'solid',
borderWidth: '1px',
outlineColor: new vscode.ThemeColor('editor.foreground'),
outlineStyle: 'solid',
outlineWidth: '1px',
})
public static create() {
let editor = vscode.window.activeTextEditor
let doc = editor ? editor.document : undefined
if (!doc) {
return undefined
}
return new CursorManager(editor!, doc)
}
private readonly originalCursorOffset: number
private readonly cachedSelections: vscode.Selection[]
constructor(
readonly editor: vscode.TextEditor,
readonly document: vscode.TextDocument
) {
this.originalCursorOffset = document.offsetAt(editor.selection.active)
this.cachedSelections = [editor.selection] // dup active selection for our working copy
this.cachedSelections.push(...editor.selections)
}
public get cursor(): vscode.Position {
return this.editor.selection.active
}
public get cursorOffset(): number {
return this.document.offsetAt(this.cursor)
}
// public get selections() : vscode.Selection[] {
// return this.editor.selections;
// }
public set selections(selections: vscode.Selection[]) {
this.editor.selections = selections
}
public get maxPosition(): number {
return this.document.offsetAt(
new vscode.Position(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
)
}
public previewCursorOffset(offset?: string): boolean {
let [newOffset, isRelative] = getOptionalNumber(offset)
let success = false
if (newOffset !== undefined) {
// Show an outline of where the cursor would be placed, and make it visible.
if (isRelative) {
newOffset! += this.originalCursorOffset
}
if (newOffset !== this.cursorOffset) {
let newPosition = this.document.positionAt(newOffset)
this.cachedSelections[0] = new vscode.Selection(
newPosition,
newPosition
)
this.editor.selections = this.cachedSelections
}
success = true
}
const range = new vscode.Range(this.cursor, this.cursor.translate(0, 1))
this.editor.setDecorations(CursorManager.cursorPositionDecoration, [range])
this.reveal()
return success
}
public commit() {
this.clearDecorations()
this.editor.selection = this.cachedSelections[0]
vscode.window.showTextDocument(this.document, {
selection: this.cachedSelections[0],
})
}
public abort() {
this.clearDecorations()
this.cachedSelections.splice(0, 1)
this.editor.selections = this.cachedSelections
this.reveal()
}
private clearDecorations(): void {
this.editor.setDecorations(CursorManager.cursorPositionDecoration, [])
}
private reveal(revealType?: vscode.TextEditorRevealType): void {
revealType =
revealType || vscode.TextEditorRevealType.InCenterIfOutsideViewport
this.editor.revealRange(this.editor.selection, revealType)
}
}