| //===--- sourcekitd-repl.cpp ----------------------------------------------===// |
| // |
| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See https://swift.org/LICENSE.txt for license information |
| // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "sourcekitd/sourcekitd.h" |
| |
| #include "llvm/ADT/ArrayRef.h" |
| #include "llvm/Support/CommandLine.h" |
| #include "llvm/Support/raw_ostream.h" |
| #include "llvm/Support/Signals.h" |
| #include "llvm/Support/FileSystem.h" |
| #include "llvm/Support/Process.h" |
| #include "llvm/Support/ConvertUTF.h" |
| #include "llvm/Support/Mutex.h" |
| #include <unistd.h> |
| #include <histedit.h> |
| using namespace llvm; |
| |
| |
| namespace { |
| template<size_t N> |
| class ConvertForWcharSize; |
| |
| template<> |
| class ConvertForWcharSize<2> { |
| public: |
| static ConversionResult ConvertFromUTF8(const char** sourceStart, |
| const char* sourceEnd, |
| wchar_t** targetStart, |
| wchar_t* targetEnd, |
| ConversionFlags flags) { |
| return ConvertUTF8toUTF16(reinterpret_cast<const UTF8**>(sourceStart), |
| reinterpret_cast<const UTF8*>(sourceEnd), |
| reinterpret_cast<UTF16**>(targetStart), |
| reinterpret_cast<UTF16*>(targetEnd), |
| flags); |
| } |
| |
| static ConversionResult ConvertToUTF8(const wchar_t** sourceStart, |
| const wchar_t* sourceEnd, |
| char** targetStart, |
| char* targetEnd, |
| ConversionFlags flags) { |
| return ConvertUTF16toUTF8(reinterpret_cast<const UTF16**>(sourceStart), |
| reinterpret_cast<const UTF16*>(sourceEnd), |
| reinterpret_cast<UTF8**>(targetStart), |
| reinterpret_cast<UTF8*>(targetEnd), |
| flags); |
| } |
| }; |
| |
| template<> |
| class ConvertForWcharSize<4> { |
| public: |
| static ConversionResult ConvertFromUTF8(const char** sourceStart, |
| const char* sourceEnd, |
| wchar_t** targetStart, |
| wchar_t* targetEnd, |
| ConversionFlags flags) { |
| return ConvertUTF8toUTF32(reinterpret_cast<const UTF8**>(sourceStart), |
| reinterpret_cast<const UTF8*>(sourceEnd), |
| reinterpret_cast<UTF32**>(targetStart), |
| reinterpret_cast<UTF32*>(targetEnd), |
| flags); |
| } |
| |
| static ConversionResult ConvertToUTF8(const wchar_t** sourceStart, |
| const wchar_t* sourceEnd, |
| char** targetStart, |
| char* targetEnd, |
| ConversionFlags flags) { |
| return ConvertUTF32toUTF8(reinterpret_cast<const UTF32**>(sourceStart), |
| reinterpret_cast<const UTF32*>(sourceEnd), |
| reinterpret_cast<UTF8**>(targetStart), |
| reinterpret_cast<UTF8*>(targetEnd), |
| flags); |
| } |
| }; |
| |
| using Convert = ConvertForWcharSize<sizeof(wchar_t)>; |
| |
| static void convertFromUTF8(llvm::StringRef utf8, |
| llvm::SmallVectorImpl<wchar_t> &out) { |
| size_t reserve = out.size() + utf8.size(); |
| out.reserve(reserve); |
| const char *utf8_begin = utf8.begin(); |
| wchar_t *wide_begin = out.end(); |
| auto res = Convert::ConvertFromUTF8(&utf8_begin, utf8.end(), |
| &wide_begin, out.data() + reserve, |
| lenientConversion); |
| assert(res == conversionOK && "utf8-to-wide conversion failed!"); |
| (void)res; |
| out.set_size(wide_begin - out.begin()); |
| } |
| |
| static void convertToUTF8(llvm::ArrayRef<wchar_t> wide, |
| llvm::SmallVectorImpl<char> &out) { |
| size_t reserve = out.size() + wide.size()*4; |
| out.reserve(reserve); |
| const wchar_t *wide_begin = wide.begin(); |
| char *utf8_begin = out.end(); |
| auto res = Convert::ConvertToUTF8(&wide_begin, wide.end(), |
| &utf8_begin, out.data() + reserve, |
| lenientConversion); |
| assert(res == conversionOK && "wide-to-utf8 conversion failed!"); |
| (void)res; |
| out.set_size(utf8_begin - out.begin()); |
| } |
| } // end anonymous namespace |
| |
| /// An arbitrary, otherwise-unused char value that editline interprets as |
| /// entering/leaving "literal mode", meaning it passes prompt characters through |
| /// to the terminal without affecting the line state. This prevents color |
| /// escape sequences from interfering with editline's internal state. |
| static constexpr wchar_t LITERAL_MODE_CHAR = L'\1'; |
| |
| /// Append a terminal escape sequence in "literal mode" so that editline |
| /// ignores it. |
| static void appendEscapeSequence(SmallVectorImpl<wchar_t> &dest, |
| llvm::StringRef src) |
| { |
| dest.push_back(LITERAL_MODE_CHAR); |
| convertFromUTF8(src, dest); |
| dest.push_back(LITERAL_MODE_CHAR); |
| } |
| |
| enum class REPLInputKind : int { |
| /// The REPL got a "quit" signal. |
| REPLQuit, |
| /// Empty whitespace-only input. |
| Empty, |
| /// A REPL directive, such as ':help'. |
| REPLDirective, |
| /// Swift source code. |
| Request, |
| }; |
| |
| /// The main REPL prompt string. |
| static const wchar_t * const PS1 = L"(SourceKit) "; |
| /// The REPL prompt string for line continuations. |
| static const wchar_t * const PS2 = L" "; |
| |
| namespace { |
| class REPLInput { |
| EditLine *e; |
| HistoryW *h; |
| size_t PromptContinuationLevel; |
| bool NeedPromptContinuation; |
| bool ShowColors; |
| bool PromptedForLine; |
| bool Outdented; |
| |
| llvm::SmallVector<wchar_t, 80> PromptString; |
| |
| /// A buffer for all lines that the user entered, but we have not parsed yet. |
| llvm::SmallString<128> CurrentLines; |
| |
| public: |
| bool Autoindent; |
| |
| REPLInput() |
| : Autoindent(true) |
| { |
| // Only show colors if both stderr and stdout have colors. |
| ShowColors = llvm::errs().has_colors() && llvm::outs().has_colors(); |
| |
| // Make sure the terminal color gets restored when the REPL is quit. |
| if (ShowColors) |
| atexit([] { |
| llvm::outs().resetColor(); |
| llvm::errs().resetColor(); |
| }); |
| |
| e = el_init("swift", stdin, stdout, stderr); |
| h = history_winit(); |
| PromptContinuationLevel = 0; |
| el_wset(e, EL_EDITOR, L"emacs"); |
| el_wset(e, EL_PROMPT_ESC, PromptFn, LITERAL_MODE_CHAR); |
| el_wset(e, EL_CLIENTDATA, (void*)this); |
| el_wset(e, EL_HIST, history, h); |
| el_wset(e, EL_SIGNAL, 1); |
| el_wset(e, EL_GETCFN, GetCharFn); |
| |
| // Provide special outdenting behavior for '}' and ':'. |
| el_wset(e, EL_ADDFN, L"swift-close-brace", L"Reduce {} indentation level", |
| BindingFn<&REPLInput::onCloseBrace>); |
| el_wset(e, EL_BIND, L"}", L"swift-close-brace", nullptr); |
| |
| el_wset(e, EL_ADDFN, L"swift-colon", L"Reduce label indentation level", |
| BindingFn<&REPLInput::onColon>); |
| el_wset(e, EL_BIND, L":", L"swift-colon", nullptr); |
| |
| // Provide special indent/completion behavior for tab. |
| el_wset(e, EL_ADDFN, L"swift-indent-or-complete", |
| L"Indent line or trigger completion", |
| BindingFn<&REPLInput::onIndentOrComplete>); |
| el_wset(e, EL_BIND, L"\t", L"swift-indent-or-complete", nullptr); |
| |
| el_wset(e, EL_ADDFN, L"swift-complete", |
| L"Trigger completion", |
| BindingFn<&REPLInput::onComplete>); |
| |
| // Provide some common bindings to complement editline's defaults. |
| // ^W should delete previous word, not the entire line. |
| el_wset(e, EL_BIND, L"\x17", L"ed-delete-prev-word", nullptr); |
| // ^_ should undo. |
| el_wset(e, EL_BIND, L"\x1f", L"vi-undo", nullptr); |
| |
| HistEventW ev; |
| history_w(h, &ev, H_SETSIZE, 800); |
| } |
| |
| ~REPLInput() { |
| if (ShowColors) |
| llvm::outs().resetColor(); |
| |
| // FIXME: This should not be needed, but seems to help when stdout is being |
| // redirected to a file. Perhaps there is some underlying editline bug |
| // where it is setting stdout into some weird state and not restoring it |
| // with el_end? |
| llvm::outs().flush(); |
| fflush(stdout); |
| el_end(e); |
| } |
| |
| REPLInputKind getREPLInput(SmallVectorImpl<char> &Result) { |
| int BraceCount = 0; |
| bool HadLineContinuation = false; |
| bool UnfinishedInfixExpr = false; |
| unsigned CurChunkLines = 0; |
| |
| CurrentLines.clear(); |
| |
| // Reset color before showing the prompt. |
| if (ShowColors) |
| llvm::outs().resetColor(); |
| |
| do { |
| // Read one line. |
| PromptContinuationLevel = BraceCount; |
| NeedPromptContinuation = BraceCount != 0 || HadLineContinuation || |
| UnfinishedInfixExpr; |
| PromptedForLine = false; |
| Outdented = false; |
| int LineCount; |
| size_t LineStart = CurrentLines.size(); |
| const wchar_t* WLine = el_wgets(e, &LineCount); |
| if (!WLine) { |
| // End-of-file. |
| if (PromptedForLine) |
| printf("\n"); |
| return REPLInputKind::REPLQuit; |
| } |
| |
| if (Autoindent) { |
| size_t indent = PromptContinuationLevel*2; |
| CurrentLines.append(indent, ' '); |
| } |
| |
| convertToUTF8(llvm::makeArrayRef(WLine, WLine + wcslen(WLine)), |
| CurrentLines); |
| |
| // Special-case backslash for line continuations in the REPL. |
| if (CurrentLines.size() > 2 && |
| CurrentLines.end()[-1] == '\n' && CurrentLines.end()[-2] == '\\') { |
| HadLineContinuation = true; |
| CurrentLines.erase(CurrentLines.end() - 2); |
| } else { |
| HadLineContinuation = false; |
| } |
| |
| // Enter the line into the line history. |
| // FIXME: We should probably be a bit more clever here about which lines |
| // we put into the history and when we put them in. |
| HistEventW ev; |
| history_w(h, &ev, H_ENTER, WLine); |
| |
| ++CurChunkLines; |
| |
| // If we detect a line starting with a colon, treat it as a special |
| // REPL escape. |
| char const *s = CurrentLines.data() + LineStart; |
| char const *p = s; |
| while (p < CurrentLines.end() && isspace(*p)) { |
| ++p; |
| } |
| if (p == CurrentLines.end()) { |
| if (BraceCount != 0 || UnfinishedInfixExpr) continue; |
| return REPLInputKind::Empty; |
| } |
| |
| UnfinishedInfixExpr = false; |
| |
| if (CurChunkLines == 1 && BraceCount == 0 && *p == ':') { |
| // Colorize the response output. |
| if (ShowColors) |
| llvm::outs().changeColor(llvm::raw_ostream::GREEN); |
| |
| Result.clear(); |
| Result.append(CurrentLines.begin(), CurrentLines.end()); |
| |
| // The lexer likes null-terminated data. |
| Result.push_back('\0'); |
| Result.pop_back(); |
| |
| return REPLInputKind::REPLDirective; |
| } |
| |
| // If we detect unbalanced braces, keep reading before |
| // we start parsing. |
| while (p < CurrentLines.end()) { |
| if (*p == '{' || *p == '(' || *p == '[') |
| ++BraceCount; |
| else if (*p == '}' || *p == ')' || *p == ']') |
| --BraceCount; |
| ++p; |
| } |
| while (isspace(*--p) && p >= s); |
| } while (BraceCount > 0 || HadLineContinuation || UnfinishedInfixExpr); |
| |
| Result.clear(); |
| Result.append(CurrentLines.begin(), CurrentLines.end()); |
| |
| // The lexer likes null-terminated data. |
| Result.push_back('\0'); |
| Result.pop_back(); |
| |
| if (ShowColors) |
| llvm::outs().resetColor(); |
| // Colorize the response output. |
| // if (ShowColors) |
| // llvm::outs().changeColor(llvm::raw_ostream::CYAN); |
| |
| return REPLInputKind::Request; |
| } |
| |
| private: |
| static wchar_t *PromptFn(EditLine *e) { |
| void* clientdata; |
| el_wget(e, EL_CLIENTDATA, &clientdata); |
| return const_cast<wchar_t*>(((REPLInput*)clientdata)->getPrompt()); |
| } |
| |
| const wchar_t *getPrompt() { |
| PromptString.clear(); |
| |
| if (ShowColors) { |
| const char *colorCode = llvm::sys::Process::OutputColor( |
| static_cast<char>(llvm::raw_ostream::YELLOW), false, false); |
| if (colorCode) |
| appendEscapeSequence(PromptString, colorCode); |
| } |
| |
| |
| if (!NeedPromptContinuation) |
| PromptString.insert(PromptString.end(), PS1, PS1 + wcslen(PS1)); |
| else { |
| PromptString.insert(PromptString.end(), PS2, PS2 + wcslen(PS2)); |
| if (Autoindent) |
| PromptString.append(2*PromptContinuationLevel, L' '); |
| } |
| |
| if (ShowColors) { |
| const char *colorCode = llvm::sys::Process::ResetColor(); |
| if (colorCode) |
| appendEscapeSequence(PromptString, colorCode); |
| } |
| |
| PromptedForLine = true; |
| PromptString.push_back(L'\0'); |
| return PromptString.data(); |
| } |
| |
| /// Custom GETCFN to reset completion state after typing. |
| static int GetCharFn(EditLine *e, wchar_t *out) { |
| void* clientdata; |
| el_wget(e, EL_CLIENTDATA, &clientdata); |
| //REPLInput *that = (REPLInput*)clientdata; |
| |
| wint_t c; |
| while (errno = 0, (c = getwc(stdin)) == WEOF) { |
| if (errno == EINTR) |
| continue; |
| *out = L'\0'; |
| return feof(stdin) ? 0 : -1; |
| } |
| |
| // If the user typed anything other than tab, reset the completion state. |
| // if (c != L'\t') |
| // that->completions.reset(); |
| *out = wchar_t(c); |
| return 1; |
| } |
| |
| template<unsigned char (REPLInput::*method)(int)> |
| static unsigned char BindingFn(EditLine *e, int ch) { |
| void *clientdata; |
| el_wget(e, EL_CLIENTDATA, &clientdata); |
| return (((REPLInput*)clientdata)->*method)(ch); |
| } |
| |
| bool isAtStartOfLine(const LineInfoW *line) { |
| for (wchar_t c : llvm::makeArrayRef(line->buffer, |
| line->cursor - line->buffer)) { |
| if (!iswspace(c)) |
| return false; |
| } |
| return true; |
| } |
| |
| // /^\s*\w+\s*:$/ |
| bool lineLooksLikeLabel(const LineInfoW *line) { |
| const wchar_t *p = line->buffer; |
| while (p != line->cursor && iswspace(*p)) |
| ++p; |
| |
| if (p == line->cursor) |
| return false; |
| |
| do { |
| ++p; |
| } while (p != line->cursor && (iswalnum(*p) || *p == L'_')); |
| |
| while (p != line->cursor && iswspace(*p)) |
| ++p; |
| |
| return p+1 == line->cursor || *p == L':'; |
| } |
| |
| // /^\s*set\s*\(.*\)\s*:$/ |
| bool lineLooksLikeSetter(const LineInfoW *line) { |
| const wchar_t *p = line->buffer; |
| while (p != line->cursor && iswspace(*p)) |
| ++p; |
| |
| if (p == line->cursor || *p++ != L's') |
| return false; |
| if (p == line->cursor || *p++ != L'e') |
| return false; |
| if (p == line->cursor || *p++ != L't') |
| return false; |
| |
| while (p != line->cursor && iswspace(*p)) |
| ++p; |
| |
| if (p == line->cursor || *p++ != L'(') |
| return false; |
| |
| if (line->cursor - p < 2 || line->cursor[-1] != L':') |
| return false; |
| |
| p = line->cursor - 1; |
| while (iswspace(*--p)); |
| |
| return *p == L')'; |
| } |
| |
| // /^\s*case.*:$/ |
| bool lineLooksLikeCase(const LineInfoW *line) { |
| const wchar_t *p = line->buffer; |
| while (p != line->cursor && iswspace(*p)) |
| ++p; |
| |
| if (p == line->cursor || *p++ != L'c') |
| return false; |
| if (p == line->cursor || *p++ != L'a') |
| return false; |
| if (p == line->cursor || *p++ != L's') |
| return false; |
| if (p == line->cursor || *p++ != L'e') |
| return false; |
| |
| return line->cursor[-1] == ':'; |
| } |
| |
| void outdent() { |
| // If we didn't already outdent, do so. |
| if (!Outdented) { |
| if (PromptContinuationLevel > 0) |
| --PromptContinuationLevel; |
| Outdented = true; |
| } |
| } |
| |
| unsigned char onColon(int ch) { |
| // Add the character to the string. |
| wchar_t s[2] = {(wchar_t)ch, 0}; |
| el_winsertstr(e, s); |
| |
| const LineInfoW *line = el_wline(e); |
| |
| // Outdent if the line looks like a label. |
| if (lineLooksLikeLabel(line)) |
| outdent(); |
| // Outdent if the line looks like a setter. |
| else if (lineLooksLikeSetter(line)) |
| outdent(); |
| // Outdent if the line looks like a 'case' label. |
| else if (lineLooksLikeCase(line)) |
| outdent(); |
| |
| return CC_REFRESH; |
| } |
| |
| unsigned char onCloseBrace(int ch) { |
| bool atStart = isAtStartOfLine(el_wline(e)); |
| |
| // Add the character to the string. |
| wchar_t s[2] = {(wchar_t)ch, 0}; |
| el_winsertstr(e, s); |
| |
| // Don't outdent if we weren't at the start of the line. |
| if (!atStart) { |
| return CC_REFRESH; |
| } |
| |
| outdent(); |
| return CC_REFRESH; |
| } |
| |
| unsigned char onIndentOrComplete(int ch) { |
| const LineInfoW *line = el_wline(e); |
| |
| // FIXME: UTF-8? What's that? |
| size_t cursorPos = line->cursor - line->buffer; |
| |
| // If there's nothing but whitespace before the cursor, indent to the next |
| // 2-character tab stop. |
| if (isAtStartOfLine(line)) { |
| const wchar_t *indent = cursorPos & 1 ? L" " : L" "; |
| el_winsertstr(e, indent); |
| return CC_REFRESH; |
| } |
| |
| // Otherwise, look for completions. |
| return onComplete(ch); |
| } |
| |
| void insertStringRef(StringRef s) { |
| if (s.empty()) |
| return; |
| // Convert s to wchar_t* and null-terminate for el_winsertstr. |
| SmallVector<wchar_t, 64> TmpStr; |
| convertFromUTF8(s, TmpStr); |
| TmpStr.push_back(L'\0'); |
| el_winsertstr(e, TmpStr.data()); |
| } |
| |
| unsigned char onComplete(int ch) { |
| return CC_REFRESH; |
| } |
| |
| }; |
| } // end anonymous namespace |
| |
| static bool handleRequest(StringRef Req, std::string &Error); |
| |
| /// Responds to a REPL input. Returns true if the repl should continue, |
| /// false if it should quit. |
| static bool handleREPLInput(REPLInputKind inputKind, llvm::StringRef Line) { |
| switch (inputKind) { |
| case REPLInputKind::REPLQuit: |
| return false; |
| |
| case REPLInputKind::Empty: |
| return true; |
| |
| case REPLInputKind::REPLDirective: |
| if (Line == ":quit\n" || Line == ":exit\n") |
| return false; |
| if (Line == ":help\n") { |
| printf("%s", "Available commands:\n" |
| " :quit - quit the interpreter (you can also use :exit " |
| "or Control+D)\n"); |
| } |
| return true; |
| |
| case REPLInputKind::Request: { |
| std::string Error; |
| if (handleRequest(Line, Error)) |
| llvm::errs() << "error: " << Error << '\n'; |
| return true; |
| } |
| } |
| } |
| |
| static bool printResponse(sourcekitd_response_t Resp) { |
| bool IsError = sourcekitd_response_is_error(Resp); |
| if (IsError) |
| sourcekitd_response_description_dump(Resp); |
| else |
| sourcekitd_response_description_dump_filedesc(Resp, STDOUT_FILENO); |
| |
| sourcekitd_response_dispose(Resp); |
| return IsError; |
| }; |
| |
| static bool handleRequest(StringRef ReqStr, std::string &ErrorMessage) { |
| bool UseAsync = false; |
| ReqStr = ReqStr.ltrim(); |
| if (ReqStr.startswith("async")) { |
| UseAsync = true; |
| ReqStr = ReqStr.substr(strlen("async")); |
| } |
| |
| SmallString<64> Str(ReqStr); |
| char *Err = nullptr; |
| sourcekitd_object_t Req = |
| sourcekitd_request_create_from_yaml(Str.c_str(), &Err); |
| if (!Req) { |
| assert(Err); |
| ErrorMessage = Err; |
| free(Err); |
| return true; |
| } |
| |
| // sourcekitd_request_description_dump(Req); |
| |
| bool IsError = false; |
| |
| if (UseAsync) { |
| static unsigned AsyncReqCount = 0; |
| static llvm::sys::Mutex AsynRespPrintMtx; |
| |
| unsigned CurrReqCount = ++AsyncReqCount; |
| llvm::raw_fd_ostream OS(STDOUT_FILENO, /*shouldClose=*/false); |
| OS << "send async request #" << CurrReqCount << '\n'; |
| sourcekitd_send_request(Req, nullptr, ^(sourcekitd_response_t Resp) { |
| llvm::sys::ScopedLock L(AsynRespPrintMtx); |
| llvm::raw_fd_ostream OS(STDOUT_FILENO, /*shouldClose=*/false); |
| OS << "received async response #" << CurrReqCount << '\n'; |
| printResponse(Resp); |
| }); |
| |
| } else { |
| sourcekitd_response_t Resp = sourcekitd_send_request_sync(Req); |
| IsError = printResponse(Resp); |
| } |
| |
| sourcekitd_request_release(Req); |
| return IsError; |
| } |
| |
| |
| int main(int argc, const char **argv) { |
| llvm::sys::PrintStackTraceOnErrorSignal(argv[0]); |
| |
| sourcekitd_initialize(); |
| |
| sourcekitd_set_notification_handler(^(sourcekitd_response_t Resp) { |
| llvm::raw_fd_ostream OS(STDOUT_FILENO, /*shouldClose=*/false); |
| OS << "received notification:" << '\n'; |
| printResponse(Resp); |
| }); |
| |
| REPLInput Inp; |
| if (llvm::sys::Process::StandardInIsUserInput()) |
| printf("%s", "Welcome to SourceKit. Type ':help' for assistance.\n"); |
| |
| llvm::SmallString<80> Line; |
| REPLInputKind inputKind; |
| do { |
| inputKind = Inp.getREPLInput(Line); |
| } while (handleREPLInput(inputKind, Line)); |
| |
| sourcekitd_shutdown(); |
| return 0; |
| } |