You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
6.2 KiB
TypeScript
134 lines
6.2 KiB
TypeScript
/* Mostly copied from https://github.com/leanprover/vscode-lean4/blob/master/lean4-infoview/src/infoview/infos.tsx */
|
|
|
|
import * as React from 'react';
|
|
import { DidChangeTextDocumentParams, DidCloseTextDocumentParams, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol';
|
|
|
|
import { EditorContext } from '../../../../node_modules/lean4-infoview/src/infoview/contexts';
|
|
import { DocumentPosition, Keyed, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult } from '../../../../node_modules/lean4-infoview/src/infoview/util';
|
|
import { Info, InfoProps } from './info';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
/** Manages and displays pinned infos, as well as info for the current location. */
|
|
export function Infos() {
|
|
let { t } = useTranslation()
|
|
const ec = React.useContext(EditorContext);
|
|
|
|
// Update pins when the document changes. In particular, when edits are made
|
|
// earlier in the text such that a pin has to move up or down.
|
|
const [pinnedPositions, setPinnedPositions] = useClientNotificationState(
|
|
'textDocument/didChange',
|
|
new Array<Keyed<DocumentPosition>>(),
|
|
(pinnedPositions, params: DidChangeTextDocumentParams) => {
|
|
if (pinnedPositions.length === 0) return pinnedPositions;
|
|
|
|
let changed: boolean = false;
|
|
const newPins = pinnedPositions.map(pin => {
|
|
if (pin.uri !== params.textDocument.uri) return pin;
|
|
// NOTE(WN): It's important to make a clone here, otherwise this
|
|
// actually mutates the pin. React state updates must be pure.
|
|
// See https://github.com/facebook/react/issues/12856
|
|
const newPin: Keyed<DocumentPosition> = { ...pin };
|
|
for (const chg of params.contentChanges) {
|
|
if (!TextDocumentContentChangeEvent.isIncremental(chg)) {
|
|
changed = true;
|
|
return null;
|
|
}
|
|
if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.start)) continue;
|
|
// We can assume chg.range.start < pin
|
|
|
|
// If the pinned position is replaced with new text, just delete the pin.
|
|
if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.end)) {
|
|
changed = true;
|
|
return null;
|
|
}
|
|
|
|
const oldPin = { ...newPin };
|
|
|
|
// How many lines before the pin position were added by the change.
|
|
// Can be negative when more lines are removed than added.
|
|
let additionalLines = 0;
|
|
let lastLineLen = chg.range.start.character;
|
|
for (const c of chg.text)
|
|
if (c === '\n') {
|
|
additionalLines++;
|
|
lastLineLen = 0;
|
|
} else lastLineLen++;
|
|
|
|
// Subtract lines that were already present
|
|
additionalLines -= (chg.range.end.line - chg.range.start.line)
|
|
newPin.line += additionalLines;
|
|
|
|
if (oldPin.line < chg.range.end.line) {
|
|
// Should never execute by the <= check above.
|
|
throw new Error('unreachable code reached')
|
|
} else if (oldPin.line === chg.range.end.line) {
|
|
newPin.character = lastLineLen + (oldPin.character - chg.range.end.character);
|
|
}
|
|
}
|
|
if (!DocumentPosition.isEqual(newPin, pin)) changed = true;
|
|
|
|
// NOTE(WN): We maintain the `key` when a pin is moved around to maintain
|
|
// its component identity and minimise flickering.
|
|
return newPin;
|
|
});
|
|
|
|
if (changed) return newPins.filter(p => p !== null) as Keyed<DocumentPosition>[];
|
|
return pinnedPositions;
|
|
},
|
|
[]
|
|
);
|
|
|
|
// Remove pins for closed documents
|
|
useClientNotificationEffect(
|
|
'textDocument/didClose',
|
|
(params: DidCloseTextDocumentParams) => {
|
|
setPinnedPositions(pinnedPositions => pinnedPositions.filter(p => p.uri !== params.textDocument.uri));
|
|
},
|
|
[]
|
|
);
|
|
|
|
const curPos: DocumentPosition | undefined =
|
|
useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined)
|
|
|
|
// Update pins on UI actions
|
|
const pinKey = React.useRef<number>(0);
|
|
const isPinned = (pinnedPositions: DocumentPosition[], pos: DocumentPosition) => {
|
|
return pinnedPositions.some(p => DocumentPosition.isEqual(p, pos));
|
|
}
|
|
const pin = React.useCallback((pos: DocumentPosition) => {
|
|
setPinnedPositions(pinnedPositions => {
|
|
if (isPinned(pinnedPositions, pos)) return pinnedPositions;
|
|
pinKey.current += 1;
|
|
return [ ...pinnedPositions, { ...pos, key: pinKey.current.toString() } ];
|
|
});
|
|
}, []);
|
|
const unpin = React.useCallback((pos: DocumentPosition) => {
|
|
setPinnedPositions(pinnedPositions => {
|
|
if (!isPinned(pinnedPositions, pos)) return pinnedPositions;
|
|
return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, pos));
|
|
});
|
|
}, []);
|
|
|
|
// Toggle pin at current position when the editor requests it
|
|
useEvent(ec.events.requestedAction, act => {
|
|
if (act.kind !== 'togglePin') return
|
|
if (!curPos) return
|
|
setPinnedPositions(pinnedPositions => {
|
|
if (isPinned(pinnedPositions, curPos)) {
|
|
return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, curPos));
|
|
} else {
|
|
pinKey.current += 1;
|
|
return [ ...pinnedPositions, { ...curPos, key: pinKey.current.toString() } ];
|
|
}
|
|
});
|
|
}, [curPos?.uri, curPos?.line, curPos?.character]);
|
|
|
|
const infoProps: Keyed<InfoProps>[] = pinnedPositions.map(pos => ({ kind: 'pin', onPin: unpin, pos, key: pos.key }));
|
|
if (curPos) infoProps.push({ kind: 'cursor', onPin: pin, key: 'cursor' });
|
|
|
|
return <div>
|
|
{infoProps.map (ps => <Info {...ps} />)}
|
|
{!curPos && <p>{t("Click somewhere in the Lean file to enable the infoview.")}</p> }
|
|
</div>;
|
|
}
|