mirror of
https://github.com/vadimmelnicuk/meo.git
synced 2026-09-25 17:35:47 +00:00
fix: restore floating toolbar and spelling suggestions with CSpell
This commit is contained in:
+8
-7
@@ -1,14 +1,15 @@
|
||||
# Markdown Editor Optimized (MEO)
|
||||
---
|
||||
## Unreleased
|
||||
- Added the theme color `activeLineBackground` to customize the active-line highlight in Live and Source
|
||||
- Added a shared Read Only toggle beside Spell Check that blocks editing in Live and Source while preserving reading, selection, copying, and external updates; the preference defaults to off and persists across documents, sessions, and workspaces
|
||||
- Added visual multiline editing to live table cells while storing line breaks as `<br>` in Markdown
|
||||
- Added link-reference definitions as visible linked lists in HTML and PDF exports, and restored copying selections across rendered blocks
|
||||
- Added configurable CodeMirror keymap via `markdownEditorOptimized.keymap`
|
||||
- Fixed live table cell clicks placing the caret at the end instead of the clicked position
|
||||
- Added a shared Read Only toggle
|
||||
- Added the theme color to customize the active-line highlight
|
||||
- Added visual multiline editing to live table cells
|
||||
- Added link-reference definitions as visible linked lists in HTML and PDF exports
|
||||
- Added configurable CodeMirror keymap in settings
|
||||
- Fixed live table cell clicks caret placing
|
||||
- Fixed the selection formatting toolbox flickering while highlighting text
|
||||
- Hardened external document sync (git/LLM/outside editors): prefer host content on conflict, fix stuck applyingExternal, Reload recovery
|
||||
- Fixed disappearing floating toolbar and missing spelling suggestions
|
||||
- Hardened external document sync
|
||||
|
||||
## 0.1.26
|
||||
- Improved dark Mermaid diagram line contrast
|
||||
|
||||
@@ -35,6 +35,7 @@ import type { ExportStyleEnvironment } from '../export/runtime';
|
||||
import type { ThemeSettings } from '../shared/themeDefaults';
|
||||
import type { RawVscodeTheme } from '../shared/vscodeTheme';
|
||||
import type { OutlinePosition } from '../shared/extensionConfig';
|
||||
import { cspellReplacementEdit } from '../spell/cspellCodeActions';
|
||||
import {
|
||||
collectMeoSpellDiagnostics,
|
||||
collectMeoSpellSuggestions,
|
||||
@@ -1679,36 +1680,45 @@ function simpleReplacementFromCodeAction(
|
||||
requestedRange: { from: number; to: number },
|
||||
action: vscode.Command | vscode.CodeAction
|
||||
): string | null {
|
||||
if (!('edit' in action) || !action.edit || ('disabled' in action && action.disabled)) {
|
||||
if ('disabled' in action && action.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entries = action.edit.entries();
|
||||
if (entries.length !== 1) {
|
||||
return null;
|
||||
let edit: vscode.TextEdit;
|
||||
if ('edit' in action && action.edit) {
|
||||
const entries = action.edit.entries();
|
||||
if (entries.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
const [uri, edits] = entries[0];
|
||||
if (uri.toString() !== document.uri.toString() || edits.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
[edit] = edits;
|
||||
} else {
|
||||
const commandEdit = cspellReplacementEdit(action, document.uri.toString(), document.version);
|
||||
if (!commandEdit) {
|
||||
return null;
|
||||
}
|
||||
edit = new vscode.TextEdit(new vscode.Range(
|
||||
commandEdit.range.start.line,
|
||||
commandEdit.range.start.character,
|
||||
commandEdit.range.end.line,
|
||||
commandEdit.range.end.character
|
||||
), commandEdit.newText);
|
||||
}
|
||||
|
||||
const [uri, edits] = entries[0];
|
||||
if (uri.toString() !== document.uri.toString() || edits.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [edit] = edits;
|
||||
const documentText = document.getText();
|
||||
const editFrom = mapDocumentOffsetToNormalizedOffset(documentText, document.offsetAt(edit.range.start));
|
||||
const editTo = mapDocumentOffsetToNormalizedOffset(documentText, document.offsetAt(edit.range.end));
|
||||
const editRange = clampDiagnosticRange(editFrom, editTo, documentText.replace(/\r\n?/g, '\n').length);
|
||||
if (!editRange || !rangesOverlap(editRange, requestedRange)) {
|
||||
if (!editRange || editRange.from !== requestedRange.from || editRange.to !== requestedRange.to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return edit.newText;
|
||||
}
|
||||
|
||||
function rangesOverlap(left: { from: number; to: number }, right: { from: number; to: number }): boolean {
|
||||
return left.from < right.to && right.from < left.to;
|
||||
}
|
||||
|
||||
async function handleSaveImageFromClipboard(
|
||||
message: SaveImageFromClipboardMessage,
|
||||
documentUri: vscode.Uri
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type * as vscode from 'vscode';
|
||||
|
||||
type Position = { line: number; character: number };
|
||||
type ReplacementEdit = { range: { start: Position; end: Position }; newText: string };
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> => (
|
||||
typeof value === 'object' && value !== null
|
||||
);
|
||||
|
||||
function isPosition(value: unknown): value is Position {
|
||||
return isRecord(value) &&
|
||||
Number.isInteger(value.line) && Number(value.line) >= 0 &&
|
||||
Number.isInteger(value.character) && Number(value.character) >= 0;
|
||||
}
|
||||
|
||||
export function cspellReplacementEdit(
|
||||
action: vscode.Command | vscode.CodeAction,
|
||||
documentUri: string,
|
||||
documentVersion: number
|
||||
): ReplacementEdit | null {
|
||||
const command = typeof action.command === 'string' ? action : action.command;
|
||||
if (!command || command.command !== 'cSpell.editText') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read CSpell's replacement without executing its command or moving editor focus.
|
||||
const args: unknown = 'arguments' in command ? command.arguments : undefined;
|
||||
if (!Array.isArray(args) || args.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
const [uri, version, edits] = args;
|
||||
if (uri !== documentUri || version !== documentVersion || !Array.isArray(edits) || edits.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const edit: unknown = edits[0];
|
||||
if (!isRecord(edit) || typeof edit.newText !== 'string' || !isRecord(edit.range)) {
|
||||
return null;
|
||||
}
|
||||
const { start, end } = edit.range;
|
||||
if (!isPosition(start) || !isPosition(end)) {
|
||||
return null;
|
||||
}
|
||||
if (end.line < start.line || (end.line === start.line && end.character <= start.character)) {
|
||||
return null;
|
||||
}
|
||||
return { range: { start, end }, newText: edit.newText };
|
||||
}
|
||||
+61
-16
@@ -53,6 +53,7 @@ import {
|
||||
} from './helpers/tables';
|
||||
import { parseFrontmatter, sourceFrontmatterField } from './helpers/frontmatter';
|
||||
import { collectLatexMathRanges } from './helpers/math';
|
||||
import type { SelectionMenuState } from './helpers/selectionMenu';
|
||||
import { diagnosticDataField, diagnosticField, setDiagnosticsEffect, type EditorDiagnostic } from './helpers/diagnostics';
|
||||
|
||||
declare module '@codemirror/view' {
|
||||
@@ -214,6 +215,7 @@ export function createEditor({
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
anchorBottomY: number;
|
||||
suggestions?: string[];
|
||||
} | null = null;
|
||||
|
||||
const expandVimLeader = (keys: string, leaderKey: string) => keys.replace(/<leader>/gi, leaderKey || '\\');
|
||||
@@ -734,13 +736,10 @@ export function createEditor({
|
||||
return coords;
|
||||
};
|
||||
|
||||
const getActiveTableSelectionState = (input) => {
|
||||
const getActiveTableSelectionState = (input: HTMLTextAreaElement): (SelectionMenuState & { from: number; to: number }) | null => {
|
||||
const selection = getTableInputDocumentSelection(input);
|
||||
if (!selection) return null;
|
||||
const diagnostic = diagnosticForRange(selection.from, selection.to);
|
||||
if (diagnostic) {
|
||||
requestDiagnosticSuggestionsFor(diagnostic, selection);
|
||||
}
|
||||
return {
|
||||
visible: true,
|
||||
from: selection.from,
|
||||
@@ -1015,6 +1014,37 @@ export function createEditor({
|
||||
diagnostic.from === from && diagnostic.to === to
|
||||
));
|
||||
|
||||
const publishSelectionMenu = (
|
||||
state: SelectionMenuState & { from?: number; to?: number }
|
||||
): void => {
|
||||
if (!state.visible || state.from === undefined || state.to === undefined) {
|
||||
clearDiagnosticSuggestionState();
|
||||
onSelectionChange?.(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const diagnostic = diagnosticForRange(state.from, state.to);
|
||||
if (!diagnostic) {
|
||||
clearDiagnosticSuggestionState();
|
||||
onSelectionChange?.(state);
|
||||
return;
|
||||
}
|
||||
|
||||
requestDiagnosticSuggestionsFor(diagnostic, {
|
||||
anchorX: state.anchorX ?? 0,
|
||||
anchorY: state.anchorY ?? 0,
|
||||
anchorBottomY: state.anchorBottomY
|
||||
});
|
||||
onSelectionChange?.({
|
||||
...state,
|
||||
diagnosticSuggestions: pendingDiagnosticSuggestionRequest?.suggestions?.map((text) => ({
|
||||
from: diagnostic.from,
|
||||
to: diagnostic.to,
|
||||
text
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
const emitSelectionChange = () => {
|
||||
if (!view || typeof onSelectionChange !== 'function') {
|
||||
return;
|
||||
@@ -1022,7 +1052,7 @@ export function createEditor({
|
||||
|
||||
const activeTableInput = getActiveTableInput();
|
||||
if (activeTableInput) {
|
||||
onSelectionChange(getActiveTableSelectionState(activeTableInput) ?? { visible: false });
|
||||
publishSelectionMenu(getActiveTableSelectionState(activeTableInput) ?? { visible: false });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1032,26 +1062,26 @@ export function createEditor({
|
||||
|
||||
const selection = view.state.selection.main;
|
||||
if (selection.empty) {
|
||||
onSelectionChange({ visible: false });
|
||||
publishSelectionMenu({ visible: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const from = Math.min(selection.from, selection.to);
|
||||
const to = Math.max(selection.from, selection.to);
|
||||
if (isSearchMatchSelection(from, to)) {
|
||||
onSelectionChange({ visible: false });
|
||||
publishSelectionMenu({ visible: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isRegularInlineSelection(view.state, from, to)) {
|
||||
onSelectionChange({ visible: false });
|
||||
publishSelectionMenu({ visible: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const align = isDiagnosticSelectionRange(from, to) ? 'start' : undefined;
|
||||
const nativeAnchor = resolveNativeSelectionAnchor();
|
||||
if (nativeAnchor) {
|
||||
onSelectionChange({
|
||||
publishSelectionMenu({
|
||||
visible: true,
|
||||
from,
|
||||
to,
|
||||
@@ -1066,7 +1096,7 @@ export function createEditor({
|
||||
const fromCoords = view.coordsAtPos(from);
|
||||
const toCoords = view.coordsAtPos(to);
|
||||
if (!fromCoords || !toCoords) {
|
||||
onSelectionChange({ visible: false });
|
||||
publishSelectionMenu({ visible: false });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1075,7 +1105,7 @@ export function createEditor({
|
||||
const anchorY = fromCharCoords ? Math.min(fromCoords.top, fromCharCoords.top) : fromCoords.top;
|
||||
const anchorBottomY = fromCharCoords ? Math.max(fromCoords.bottom, fromCharCoords.bottom) : fromCoords.bottom;
|
||||
|
||||
onSelectionChange({
|
||||
publishSelectionMenu({
|
||||
visible: true,
|
||||
from,
|
||||
to,
|
||||
@@ -1771,6 +1801,10 @@ export function createEditor({
|
||||
syncGitGutterVisibility();
|
||||
emitSearchStateChange();
|
||||
|
||||
if (update.docChanged) {
|
||||
clearDiagnosticSuggestionState();
|
||||
}
|
||||
|
||||
if (update.selectionSet) {
|
||||
syncSelectionClass();
|
||||
emitSelectionChange();
|
||||
@@ -1779,10 +1813,6 @@ export function createEditor({
|
||||
onViewportChange?.();
|
||||
}
|
||||
|
||||
if (update.docChanged) {
|
||||
clearDiagnosticSuggestionState();
|
||||
}
|
||||
|
||||
if (!update.docChanged || applyingExternal || applyingRenumber) {
|
||||
return;
|
||||
}
|
||||
@@ -2322,8 +2352,17 @@ export function createEditor({
|
||||
},
|
||||
setDiagnostics(diagnostics: EditorDiagnostic[]) {
|
||||
currentDiagnostics = Array.isArray(diagnostics) ? diagnostics : [];
|
||||
clearDiagnosticSuggestionState();
|
||||
// Providers can publish diagnostics while a suggestion request is in flight.
|
||||
const keepSuggestionRequest = pendingDiagnosticSuggestionRequest !== null && currentDiagnostics.some(
|
||||
(diagnostic) => diagnosticKey(diagnostic) === pendingDiagnosticSuggestionRequest?.key
|
||||
);
|
||||
if (!keepSuggestionRequest) {
|
||||
clearDiagnosticSuggestionState();
|
||||
}
|
||||
view.dispatch({ effects: setDiagnosticsEffect.of(currentDiagnostics) });
|
||||
if (!keepSuggestionRequest) {
|
||||
emitSelectionChange();
|
||||
}
|
||||
},
|
||||
showDiagnosticSuggestions(requestId, payload) {
|
||||
if (
|
||||
@@ -2347,6 +2386,12 @@ export function createEditor({
|
||||
return;
|
||||
}
|
||||
|
||||
pendingDiagnosticSuggestionRequest.suggestions = payload.suggestions;
|
||||
// Pointer-up will render the completed selection, including fast responses.
|
||||
if (selectionPointerId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelectionChange?.({
|
||||
visible: true,
|
||||
from: payload.from,
|
||||
|
||||
@@ -1053,8 +1053,6 @@ const focusEditorFromHost = () => {
|
||||
const applyDiagnosticsFromHost = (diagnostics: unknown): void => {
|
||||
const nextDiagnostics = Array.isArray(diagnostics) ? diagnostics : [];
|
||||
pendingDiagnostics = nextDiagnostics;
|
||||
pendingDiagnosticSuggestionRequests.clear();
|
||||
selectionMenuController.hide();
|
||||
editor?.setDiagnostics?.(nextDiagnostics);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user