fix(editor): preserve IME composition in comments
This commit is contained in:
parent
a017e04d4c
commit
c9d655324d
|
|
@ -181,6 +181,8 @@ let semanticDiagnosticTimer: ReturnType<typeof setTimeout> | null = null;
|
|||
let semanticDiagnosticRunId = 0;
|
||||
let editorIsActive = true;
|
||||
let tableReferenceDropListenerRegistered = false;
|
||||
let imeCompositionActive = false;
|
||||
let pendingImeModelEmit = false;
|
||||
|
||||
function editorThemeAppearance() {
|
||||
return isDark.value ? "dark" : "light";
|
||||
|
|
@ -904,6 +906,7 @@ async function provideElasticsearchCompletions(currentState: import("@codemirror
|
|||
}
|
||||
|
||||
async function provideSqlCompletions(currentState: import("@codemirror/state").EditorState, position: number, explicit: boolean) {
|
||||
if (imeCompositionActive || view.value?.compositionStarted || view.value?.composing) return null;
|
||||
if (!props.connectionId) return null;
|
||||
const fullDoc = currentState.doc.toString();
|
||||
if (props.databaseType === "elasticsearch") {
|
||||
|
|
@ -990,6 +993,23 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
|
|||
}
|
||||
}
|
||||
|
||||
function isEditorComposing(currentView: EditorViewType): boolean {
|
||||
return imeCompositionActive || currentView.compositionStarted || currentView.composing;
|
||||
}
|
||||
|
||||
function flushImeComposition() {
|
||||
const currentView = view.value;
|
||||
if (!currentView || !pendingImeModelEmit) return;
|
||||
pendingImeModelEmit = false;
|
||||
emit("update:modelValue", currentView.state.doc.toString());
|
||||
scheduleSemanticDiagnostics();
|
||||
syncContextMenuState(currentView);
|
||||
emit("selectionChange", selectedSqlFromView(currentView));
|
||||
emit("cursorChange", currentView.state.selection.main.head);
|
||||
latestSelection = readEditorSelection(currentView);
|
||||
if (editorIsActive) emitEditorSelection(latestSelection);
|
||||
}
|
||||
|
||||
function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getSqlCompletionContext>, fullDoc: string, position: number) {
|
||||
if (!props.connectionId || props.database == null) return null;
|
||||
const databaseNames = supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable ? connectionStore.lookupLocalCompletionDatabases(props.connectionId, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES) : [];
|
||||
|
|
@ -1547,14 +1567,19 @@ onMounted(async () => {
|
|||
rectangularSelection({ eventFilter: (e: MouseEvent) => e.altKey || e.button === 1 }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
emit("update:modelValue", update.state.doc.toString());
|
||||
scheduleSemanticDiagnostics();
|
||||
let insertedText = "";
|
||||
update.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => {
|
||||
insertedText += inserted.toString();
|
||||
});
|
||||
if (insertedText.endsWith(".")) {
|
||||
startCompletion(update.view);
|
||||
if (isEditorComposing(update.view)) {
|
||||
pendingImeModelEmit = true;
|
||||
completionEpoch++;
|
||||
} else {
|
||||
emit("update:modelValue", update.state.doc.toString());
|
||||
scheduleSemanticDiagnostics();
|
||||
let insertedText = "";
|
||||
update.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => {
|
||||
insertedText += inserted.toString();
|
||||
});
|
||||
if (insertedText.endsWith(".")) {
|
||||
startCompletion(update.view);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (update.selectionSet || update.docChanged) {
|
||||
|
|
@ -1586,6 +1611,16 @@ onMounted(async () => {
|
|||
if (editorIsActive) emitEditorSelection(latestSelection);
|
||||
return false;
|
||||
},
|
||||
compositionstart() {
|
||||
imeCompositionActive = true;
|
||||
completionEpoch++;
|
||||
return false;
|
||||
},
|
||||
compositionend() {
|
||||
imeCompositionActive = false;
|
||||
window.setTimeout(flushImeComposition, 0);
|
||||
return false;
|
||||
},
|
||||
wheel(event) {
|
||||
if (!event.metaKey && !event.ctrlKey) return false;
|
||||
event.preventDefault();
|
||||
|
|
@ -1736,6 +1771,7 @@ watch(
|
|||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (view.value && val !== view.value.state.doc.toString()) {
|
||||
if (isEditorComposing(view.value)) return;
|
||||
view.value.dispatch({
|
||||
changes: { from: 0, to: view.value.state.doc.length, insert: val },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,7 @@ export function buildSqlCompletionItemsFromContext(
|
|||
}
|
||||
|
||||
export function shouldAutoOpenSqlCompletion(sql: string, cursor: number): boolean {
|
||||
if (isSqlCommentContext(sql, cursor)) return false;
|
||||
const previousChar = sql[cursor - 1];
|
||||
if (!previousChar) return false;
|
||||
if (/\bon\s+$/i.test(sql.slice(0, cursor))) return true;
|
||||
|
|
@ -1053,6 +1054,82 @@ export function shouldAutoOpenSqlCompletion(sql: string, cursor: number): boolea
|
|||
return /[\w$.@]/.test(previousChar);
|
||||
}
|
||||
|
||||
export function isSqlCommentContext(sql: string, cursor: number): boolean {
|
||||
const end = Math.max(0, Math.min(cursor, sql.length));
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
let inBacktick = false;
|
||||
let inBracket = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
|
||||
for (let index = 0; index < end; index += 1) {
|
||||
const ch = sql[index] ?? "";
|
||||
const next = sql[index + 1] ?? "";
|
||||
|
||||
if (inLineComment) {
|
||||
if (ch === "\n" || ch === "\r") inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (ch === "*" && next === "/") {
|
||||
inBlockComment = false;
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingleQuote) {
|
||||
if (ch === "\\" && next) {
|
||||
index += 1;
|
||||
} else if (ch === "'" && next === "'") {
|
||||
index += 1;
|
||||
} else if (ch === "'") {
|
||||
inSingleQuote = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inDoubleQuote) {
|
||||
if (ch === "\\" && next) {
|
||||
index += 1;
|
||||
} else if (ch === '"' && next === '"') {
|
||||
index += 1;
|
||||
} else if (ch === '"') {
|
||||
inDoubleQuote = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inBacktick) {
|
||||
if (ch === "`") inBacktick = false;
|
||||
continue;
|
||||
}
|
||||
if (inBracket) {
|
||||
if (ch === "]") inBracket = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "-" && next === "-") {
|
||||
inLineComment = true;
|
||||
index += 1;
|
||||
} else if (ch === "#") {
|
||||
inLineComment = true;
|
||||
} else if (ch === "/" && next === "*") {
|
||||
inBlockComment = true;
|
||||
index += 1;
|
||||
} else if (ch === "'") {
|
||||
inSingleQuote = true;
|
||||
} else if (ch === '"') {
|
||||
inDoubleQuote = true;
|
||||
} else if (ch === "`") {
|
||||
inBacktick = true;
|
||||
} else if (ch === "[") {
|
||||
inBracket = true;
|
||||
}
|
||||
}
|
||||
|
||||
return inLineComment || inBlockComment;
|
||||
}
|
||||
|
||||
export function isSqlLikeCompletionStatement(sql: string, cursor: number): boolean {
|
||||
const statement = extractStatementAt(sql, cursor).trimStart();
|
||||
if (/^(select|with)\b/i.test(statement)) return true;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
buildSqlCompletionItems,
|
||||
getSqlFunctionSignatureHelp,
|
||||
getSqlCompletionResultValidFor,
|
||||
isSqlCommentContext,
|
||||
shouldAutoOpenSqlCompletion,
|
||||
extractCteDefinitions,
|
||||
getSqlCompletionContext,
|
||||
|
|
@ -520,6 +521,22 @@ test("does not auto-open completion after structural punctuation", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("does not auto-open completion inside SQL comments", () => {
|
||||
for (const { sql, cursor } of [
|
||||
{ sql: "-- sougou", cursor: "-- sougou".length },
|
||||
{ sql: "select 1 -- sougou", cursor: "select 1 -- sougou".length },
|
||||
{ sql: "# sougou", cursor: "# sougou".length },
|
||||
{ sql: "select /* sougou */ 1", cursor: "select /* sougou".length },
|
||||
{ sql: "select /* sougou", cursor: "select /* sougou".length },
|
||||
]) {
|
||||
assert.equal(shouldAutoOpenSqlCompletion(sql, cursor), false, sql);
|
||||
}
|
||||
assert.equal(isSqlCommentContext("select '-- not comment' as value", "select '-- not comment'".length), false);
|
||||
assert.equal(isSqlCommentContext("select /* comment */ val", "select /* comment */ val".length), false);
|
||||
assert.equal(shouldAutoOpenSqlCompletion("select '-- not comment' as value", "select '-- not comment' as value".length), true);
|
||||
assert.equal(shouldAutoOpenSqlCompletion("select /* comment */ val", "select /* comment */ val".length), true);
|
||||
});
|
||||
|
||||
test("auto-opens completion after word characters and explicit dot qualifiers", () => {
|
||||
for (const sql of ["sel", "select * from us", "select u."]) {
|
||||
assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), true, sql);
|
||||
|
|
|
|||
Loading…
Reference in New Issue