| 64 | bool follows_access_operator(llvm::StringRef text, std::uint32_t offset) { |
| 65 | auto before = text.take_front(offset); |
| 66 | if(before.ends_with("::")) { |
| 67 | return true; |
| 68 | } |
| 69 | /// `x-->y` is a postfix decrement followed by `>`. |
| 70 | if(before.ends_with("->")) { |
| 71 | return !before.ends_with("-->"); |
| 72 | } |
| 73 | if(!before.ends_with(".") || before.ends_with("..")) { |
| 74 | return false; |
| 75 | } |
| 76 | /// A dot right after a numeric literal continues the literal (`3.`). |
| 77 | auto operand = before.drop_back(1); |
| 78 | auto start = operand.size(); |
| 79 | while(start > 0 && is_identifier_char(operand[start - 1])) { |
| 80 | start -= 1; |
| 81 | } |
| 82 | return start == operand.size() || !clang::isDigit(operand[start]); |
| 83 | } |
| 84 | |
| 85 | PreambleCompletionContext detect_completion_context(llvm::StringRef text, std::uint32_t offset) { |
| 86 | // TODO: cache newline offsets from incremental text updates to avoid |
| 87 | // the linear line-boundary scans on every completion trigger. |
| 88 | // |
| 89 | // The full (NUL-terminated) text is lexed and the cursor applies as a |
| 90 | // logical bound: a keyword must end at or before it to count as typed. |
| 91 | auto lexer = Lexer::from_line(text, offset); |
| 92 | auto before_cursor = [&](const Token& token) { |
| 93 | return token.range.end <= offset; |
| 94 | }; |
| 95 | |
| 96 | auto first = lexer.advance(); |
| 97 | |
| 98 | if(first.is_directive_hash() && before_cursor(first)) { |
| 99 | auto keyword = lexer.advance(); |
| 100 | if(!keyword.is_identifier() || !before_cursor(keyword) || keyword.text(text) != "include") { |
| 101 | return {}; |
| 102 | } |
| 103 | // The argument is likely half-typed, so its prefix is taken |
| 104 | // textually between the keyword token and the cursor. |
| 105 | auto argument = text.slice(keyword.range.end, offset).ltrim(); |
| 106 | CompletionContext kind; |
| 107 | if(argument.consume_front("\"")) { |
| 108 | kind = CompletionContext::IncludeQuoted; |
| 109 | } else if(argument.consume_front("<")) { |
| 110 | kind = CompletionContext::IncludeAngled; |
| 111 | } else { |
| 112 | return {}; |
| 113 | } |
| 114 | auto slash = argument.rfind('/'); |
| 115 | auto component = slash == llvm::StringRef::npos ? argument : argument.drop_front(slash + 1); |
| 116 | // A closed directive may name a file with spaces; an unclosed one |
| 117 | // ends its name at the first space. |
| 118 | char closer = kind == CompletionContext::IncludeQuoted ? '"' : '>'; |
| 119 | auto line = text.slice(offset, text.find_first_of("\r\n", offset)); |
| 120 | bool closed = line.contains(closer); |
| 121 | auto end = offset; |
| 122 | while(end < offset + line.size() && text[end] != '/' && text[end] != closer && |
no test coverage detected