import { locate } from '../parser/locate'; import type { Situation } from '../parser/types'; function at(input: string, offset = input.length): Situation { return locate(input, offset); } describe('KQL locate', () => { // ── NONE ── it('returns NONE for empty input', () => { expect(at('').kind).toBe('NONE'); }); it('returns NONE after not keyword', () => { expect(at('not ').kind).toBe('NONE'); }); it('returns NONE after and/or keywords at top level', () => { expect(at('response:200 and ').kind).toBe('NONE'); expect(at('response:200 or ').kind).toBe('NONE'); }); it('carries the typed prefix through NONE', () => { const s = at('response:200 and res'); expect(s.kind).toBe('NONE'); expect(s.prefix).toBe('res'); expect(s.replaceRange).toEqual({ start: 17, end: 20 }); }); it('returns NONE after a plain group paren', () => { expect(at('not (').kind).toBe('NONE'); }); // ── IN_FIELD ── it('returns IN_FIELD when typing a field name', () => { const s = at('resp'); expect(s.kind).toBe('IN_FIELD'); expect(s.prefix).toBe('resp'); expect(s.replaceRange).toEqual({ start: 0, end: 4 }); }); // ── AFTER_FIELD ── it('returns AFTER_FIELD after a complete field plus whitespace', () => { const s = at('response '); expect(s.kind).toBe('AFTER_FIELD'); // absorbed trailing whitespace is covered by the replace range expect(s.replaceRange).toEqual({ start: 8, end: 9 }); }); // ── IN_VALUE ── it('returns IN_VALUE right after colon', () => { const s = at('response:'); expect(s.kind).toBe('IN_VALUE'); expect(s.fieldName).toBe('response'); }); it('returns IN_VALUE after colon and whitespace', () => { const s = at('response: '); expect(s.kind).toBe('IN_VALUE'); expect(s.fieldName).toBe('response'); }); it('returns IN_VALUE while typing a value', () => { const s = at('response:2', 10); expect(s.kind).toBe('IN_VALUE'); expect(s.prefix).toBe('2'); expect(s.replaceRange).toEqual({ start: 9, end: 10 }); }); it('returns AFTER_EXPRESSION after a completed value word with trailing whitespace', () => { const s = at('response:200 '); expect(s.kind).toBe('AFTER_EXPRESSION'); expect(s.replaceRange).toEqual({ start: 13, end: 13 }); }); it('returns AFTER_EXPRESSION after a completed spaced value with trailing whitespace', () => { expect(at('response: 200 ').kind).toBe('AFTER_EXPRESSION'); }); it('returns AFTER_EXPRESSION after a completed range value with trailing whitespace', () => { expect(at('response >= 400 ').kind).toBe('AFTER_EXPRESSION'); }); it('returns IN_VALUE for cursor at end of a complete value', () => { const s = at('response:200'); expect(s.kind).toBe('IN_VALUE'); expect(s.prefix).toBe('200'); expect(s.replaceRange).toEqual({ start: 9, end: 12 }); }); it('returns IN_VALUE after range operators', () => { const s = at('response >= '); expect(s.kind).toBe('IN_VALUE'); expect(s.fieldName).toBe('response'); }); it('returns IN_VALUE with insideString inside an open quoted string', () => { const s = at('response: "win', 14); expect(s.kind).toBe('IN_VALUE'); expect(s.insideString).toBe(true); expect(s.prefix).toBe('win'); expect(s.replaceRange).toEqual({ start: 11, end: 14 }); }); it('returns IN_VALUE when cursor is inside a closed quoted string', () => { const s = at('response: "win 10"', 16); expect(s.kind).toBe('IN_VALUE'); expect(s.insideString).toBe(true); expect(s.prefix).toBe('win 1'); }); // ── AFTER_EXPRESSION ── it('returns AFTER_EXPRESSION after a closed quoted string', () => { expect(at('response: "win 10"').kind).toBe('AFTER_EXPRESSION'); }); it('returns AFTER_EXPRESSION after a closed group', () => { expect(at('not (response:500)').kind).toBe('AFTER_EXPRESSION'); }); it('returns AFTER_EXPRESSION after a closed value list', () => { expect(at('machine.os: (osx)').kind).toBe('AFTER_EXPRESSION'); }); it('returns AFTER_EXPRESSION after a closed nested query', () => { expect(at('user: { name: admin }').kind).toBe('AFTER_EXPRESSION'); }); // ── IN_VALUE_LIST ── it('returns IN_VALUE_LIST right after field:(', () => { const s = at('machine.os: ('); expect(s.kind).toBe('IN_VALUE_LIST'); expect(s.fieldName).toBe('machine.os'); }); it('returns IN_VALUE_LIST while typing a list value', () => { const s = at('machine.os: (os'); expect(s.kind).toBe('IN_VALUE_LIST'); expect(s.prefix).toBe('os'); expect(s.fieldName).toBe('machine.os'); }); it('returns IN_VALUE_LIST after a completed list value', () => { const s = at('machine.os: (osx '); expect(s.kind).toBe('IN_VALUE_LIST'); expect(s.prefix).toBe('osx'); }); it('returns IN_VALUE_LIST after and/or inside a list', () => { const s = at('machine.os: (osx or '); expect(s.kind).toBe('IN_VALUE_LIST'); expect(s.fieldName).toBe('machine.os'); }); it('returns IN_VALUE_LIST with insideString inside list strings', () => { const s = at('machine.os: ("win', 17); expect(s.kind).toBe('IN_VALUE_LIST'); expect(s.insideString).toBe(true); }); it('returns IN_VALUE_LIST before the closing paren', () => { const s = at('machine.os: (osx)', 16); expect(s.kind).toBe('IN_VALUE_LIST'); }); // ── IN_NESTED ── it('returns IN_NESTED right after path:{', () => { const s = at('user: {'); expect(s.kind).toBe('IN_NESTED'); expect(s.nestedPath).toBe('user'); }); it('returns IN_NESTED while typing a sub field', () => { const s = at('user: { na'); expect(s.kind).toBe('IN_NESTED'); expect(s.nestedPath).toBe('user'); expect(s.prefix).toBe('na'); }); it('chains nested paths', () => { const s = at('user: { tags: { ', 16); expect(s.kind).toBe('IN_NESTED'); expect(s.nestedPath).toBe('user.tags'); }); it('returns dotted fieldName for values inside nested queries', () => { const s = at('user: { name:'); expect(s.kind).toBe('IN_VALUE'); expect(s.fieldName).toBe('user.name'); }); it('returns dotted fieldName for value lists inside nested queries', () => { const s = at('user: { tags: (ad'); expect(s.kind).toBe('IN_VALUE_LIST'); expect(s.fieldName).toBe('user.tags'); }); it('returns IN_NESTED after a sub expression keyword', () => { const s = at('user: { name: admin and '); expect(s.kind).toBe('IN_NESTED'); expect(s.nestedPath).toBe('user'); }); });