neovimconnector.cpp
1 #include "neovimconnector.h" 2 3 #include <QFileInfo> 4 #include <QLocalSocket> 5 #include <QMetaMethod> 6 #include <QTcpSocket> 7 #include <QtGlobal> 8 9 #include "compat.h" 10 #include "msgpackiodevice.h" 11 #include "msgpackrequest.h" 12 #include "neovimconnectorhelper.h" 13 14 namespace NeovimQt { 15 16 /** 17 * \class NeovimQt::NeovimConnector 18 * 19 * \brief A Connection to a Neovim instance 20 * 21 */ 22 23 /** 24 * Create a new Neovim API connection from an open IO device 25 */ 26 NeovimConnector::NeovimConnector(QIODevice *dev) 27 :NeovimConnector(new MsgpackIODevice(dev)) 28 { 29 } 30 31 NeovimConnector::NeovimConnector(MsgpackIODevice *dev) 32 :QObject(), m_dev(dev) 33 { 34 m_helper = new NeovimConnectorHelper(this); 35 qRegisterMetaType<NeovimError>("NeovimError"); 36 qRegisterMetaType<int64_t>("int64_t"); 37 38 connect(m_dev, &MsgpackIODevice::error, 39 this, &NeovimConnector::msgpackError); 40 connect(m_dev, &MsgpackIODevice::aboutToClose, this, &NeovimConnector::aboutToClose); 41 42 m_dev->setParent(this); 43 44 if ( !m_dev->isOpen() ) { 45 return; 46 } 47 discoverMetadata(); 48 } 49 50 void NeovimConnector::setRequestTimeout(int ms) 51 { 52 this->m_timeout = ms; 53 } 54 55 /** 56 * Sets latest error code and message for this connector 57 */ 58 void NeovimConnector::setError(NeovimError err, const QString& msg) 59 { 60 m_ready = false; 61 if (m_error == NoError && err != NoError) { 62 m_error = err; 63 m_errorString = msg; 64 qWarning() << "Neovim fatal error" << m_errorString; 65 emit error(m_error); 66 } else { 67 // Only the first error is raised 68 qDebug() << "(Ignored) Neovim fatal error" << msg; 69 } 70 } 71 72 /** Reset error state */ 73 void NeovimConnector::clearError() 74 { 75 m_error = NoError; 76 m_errorString = ""; 77 } 78 79 /** 80 * Called when an error takes place 81 */ 82 NeovimConnector::NeovimError NeovimConnector::errorCause() 83 { 84 return m_error; 85 } 86 87 /** 88 * An human readable error message for the last error 89 */ 90 QString NeovimConnector::errorString() 91 { 92 return m_errorString; 93 } 94 95 QString NeovimConnector::connectionDescription() 96 { 97 switch (m_ctype) { 98 case SpawnedConnection: 99 return m_spawnExe + " " + m_spawnArgs.join(" "); 100 case HostConnection: 101 return m_connHost + ":" + QString::number(m_connPort); 102 case SocketConnection: 103 return m_connSocket; 104 default: 105 return ""; 106 } 107 } 108 109 /** 110 * Returns the channel id used by Neovim to identify this connection 111 */ 112 uint64_t NeovimConnector::channel() 113 { 114 return m_channel; 115 } 116 117 /** 118 * Request API information from Neovim 119 */ 120 void NeovimConnector::discoverMetadata() 121 { 122 MsgpackRequest *r = m_dev->startRequestUnchecked("vim_get_api_info", 0); 123 connect(r, &MsgpackRequest::finished, 124 m_helper, &NeovimConnectorHelper::handleMetadata); 125 connect(r, &MsgpackRequest::error, 126 m_helper, &NeovimConnectorHelper::handleMetadataError); 127 connect(r, &MsgpackRequest::timeout, 128 this, &NeovimConnector::fatalTimeout); 129 r->setTimeout(m_timeout); 130 } 131 132 /** 133 * True if the Neovim instance is ready 134 * @see ready 135 */ 136 bool NeovimConnector::isReady() 137 { 138 return m_ready; 139 } 140 141 /** 142 * @warning Do not call this before NeovimConnector::ready as been signaled 143 * @see NeovimConnector::isReady 144 */ 145 NeovimApi0* NeovimConnector::api0() 146 { 147 if ( !m_api0 ) { 148 if (m_api_compat <= 0) { 149 m_api0 = new NeovimApi0(this); 150 } else { 151 qDebug() << "This instance of neovim DOES NOT support api level 0"; 152 } 153 } 154 return m_api0; 155 } 156 /** 157 * @warning Do not call this before NeovimConnector::ready as been signaled 158 * @see NeovimConnector::isReady 159 */ 160 NeovimApi1* NeovimConnector::api1() 161 { 162 if ( !m_api1 ) { 163 if (m_api_compat <= 1 && 1 <= m_api_supported) { 164 m_api1 = new NeovimApi1(this); 165 } else { 166 qDebug() << "This instance of neovim DOES NOT support api level 1"; 167 } 168 } 169 return m_api1; 170 } 171 172 /** For compatibility with older versions */ 173 NeovimApi1* NeovimConnector::neovimObject() 174 { 175 return api1(); 176 } 177 178 /** 179 * @warning Do not call this before NeovimConnector::ready as been signaled 180 * @see NeovimConnector::isReady 181 */ 182 NeovimApi2* NeovimConnector::api2() 183 { 184 if ( !m_api2 ) { 185 if (m_api_compat <= 2 && 2 <= m_api_supported) { 186 m_api2 = new NeovimApi2(this); 187 } else { 188 qWarning() << "This instance of neovim not support api level 2"; 189 } 190 } 191 return m_api2; 192 } 193 194 /** 195 * @warning Do not call this before NeovimConnector::ready as been signaled 196 * @see NeovimConnector::isReady 197 */ 198 NeovimApi3* NeovimConnector::api3() 199 { 200 if ( !m_api3 ) { 201 if (m_api_compat <= 3 && 3 <= m_api_supported) { 202 m_api3 = new NeovimApi3(this); 203 } else { 204 qWarning() << "This instance of neovim not support api level 3"; 205 } 206 } 207 return m_api3; 208 } 209 210 /** 211 * @warning Do not call this before NeovimConnector::ready as been signaled 212 * @see NeovimConnector::isReady 213 */ 214 NeovimApi4* NeovimConnector::api4() 215 { 216 if ( !m_api4 ) { 217 if (m_api_compat <= 4 && 4 <= m_api_supported) { 218 m_api4 = new NeovimApi4(this); 219 } else { 220 qWarning() << "This instance of neovim not support api level 4"; 221 } 222 } 223 return m_api4; 224 } 225 226 /** 227 * @warning Do not call this before NeovimConnector::ready as been signaled 228 * @see NeovimConnector::isReady 229 */ 230 NeovimApi5* NeovimConnector::api5() 231 { 232 if ( !m_api5 ) { 233 if (m_api_compat <= 5 && 5 <= m_api_supported) { 234 m_api5 = new NeovimApi5(this); 235 } else { 236 qWarning() << "This instance of neovim not support api level 5"; 237 } 238 } 239 return m_api5; 240 } 241 242 /** 243 * @warning Do not call this before NeovimConnector::ready as been signaled 244 * @see NeovimConnector::isReady 245 */ 246 NeovimApi6* NeovimConnector::api6() 247 { 248 if ( !m_api6 ) { 249 if (m_api_compat <= 6 && 6 <= m_api_supported) { 250 m_api6 = new NeovimApi6(this); 251 } else { 252 qWarning() << "This instance of neovim not support api level 6"; 253 } 254 } 255 return m_api6; 256 } 257 258 /** 259 * Launch an embedded Neovim process 260 * @see processExited 261 */ 262 NeovimConnector* NeovimConnector::spawn(const QStringList& params, const QString& exe) 263 { 264 QProcess *p = new QProcess(); 265 QStringList args; 266 // Neovim accepts a `--' argument after which only filenames are passed. 267 // If the user has supplied it, our arguments must appear before. 268 if (params.indexOf("--") == -1) { 269 args << "--embed"; 270 args.append(params); 271 } else { 272 int idx = params.indexOf("--"); 273 args.append(params.mid(0, idx)); 274 args << "--embed"; 275 args.append(params.mid(idx)); 276 } 277 278 NeovimConnector *c = new NeovimConnector(p); 279 c->m_ctype = SpawnedConnection; 280 c->m_spawnArgs = params; 281 c->m_spawnExe = exe; 282 283 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) 284 connect(p, SIGNAL(error(QProcess::ProcessError)), 285 c, SLOT(processError(QProcess::ProcessError))); 286 #else 287 connect(p, &QProcess::errorOccurred, c, &NeovimConnector::processError); 288 #endif 289 connect(p, SIGNAL(finished(int,QProcess::ExitStatus)), 290 c, SIGNAL(processExited(int))); 291 connect(p, &QProcess::started, 292 c, &NeovimConnector::discoverMetadata); 293 p->start(exe, args); 294 return c; 295 } 296 297 /** 298 * Connect to Neovim using a local UNIX socket. 299 * 300 * This method also works in Windows, using named pipes. 301 * 302 * @see QLocalSocket 303 */ 304 NeovimConnector* NeovimConnector::connectToSocket(const QString& path) 305 { 306 QLocalSocket *s = new QLocalSocket(); 307 NeovimConnector *c = new NeovimConnector(s); 308 c->m_ctype = SocketConnection; 309 #if defined(Q_OS_UNIX) 310 // #936: Qt assumes paths relative to /tmp 311 // https://bugreports.qt.io/browse/QTBUG-50877 312 c->m_connSocket = QFileInfo(path).absoluteFilePath(); 313 #else 314 c->m_connSocket = path; 315 #endif 316 317 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) 318 connect(s, SIGNAL(error(QLocalSocket::LocalSocketError)), 319 c, SLOT(socketError())); 320 #else 321 connect(s, &QLocalSocket::errorOccurred, c, &NeovimConnector::socketError); 322 #endif 323 connect(s, &QLocalSocket::connected, 324 c, &NeovimConnector::discoverMetadata); 325 s->connectToServer(c->m_connSocket); 326 return c; 327 } 328 329 /** 330 * Connect to a Neovim through a TCP connection 331 * 332 * @param host is a valid hostname or IP address 333 * @param port is the TCP port 334 */ 335 NeovimConnector* NeovimConnector::connectToHost(const QString& host, int port) 336 { 337 QTcpSocket *s = new QTcpSocket(); 338 NeovimConnector *c = new NeovimConnector(s); 339 340 c->m_ctype = HostConnection; 341 c->m_connHost = host; 342 c->m_connPort = port; 343 344 #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) 345 connect(s, SIGNAL(error(QAbstractSocket::SocketError)), 346 c, SLOT(socketError())); 347 #else 348 connect(s, &QAbstractSocket::errorOccurred, c, &NeovimConnector::socketError); 349 #endif 350 connect(s, &QAbstractSocket::connected, 351 c, &NeovimConnector::discoverMetadata); 352 s->connectToHost(host, port); 353 return c; 354 } 355 356 /** 357 * Connect to a running instance of Neovim (if available). 358 * 359 * This method gets the Neovim endpoint from the NVIM_LISTEN_ADDRESS environment 360 * variable, if it is not available a new Neovim instance is spawned(). 361 * 362 * @see spawn() 363 */ 364 NeovimConnector* NeovimConnector::connectToNeovim(const QString& server) 365 { 366 QString addr = server; 367 if (addr.isEmpty()) { 368 addr = QString::fromLocal8Bit(qgetenv("NVIM_LISTEN_ADDRESS")); 369 } 370 if (addr.isEmpty()) { 371 return spawn(); 372 } 373 374 int colon_pos = addr.lastIndexOf(':'); 375 if (colon_pos != -1 && colon_pos != 0 && addr[colon_pos-1] != ':') { 376 bool ok; 377 int port = midString(addr, colon_pos + 1).toInt(&ok); 378 if (ok) { 379 QString host = addr.mid(0, colon_pos); 380 return connectToHost(host, port); 381 } 382 } 383 return connectToSocket(addr); 384 } 385 386 NeovimConnector* NeovimConnector::fromStdinOut() 387 { 388 return new NeovimConnector(MsgpackIODevice::fromStdinOut()); 389 } 390 391 /** 392 * Called when running embedded Neovim to report an error 393 * with the Neovim process 394 */ 395 void NeovimConnector::processError(QProcess::ProcessError err) 396 { 397 switch(err) { 398 case QProcess::FailedToStart: 399 setError(FailedToStart, m_dev->errorString()); 400 break; 401 case QProcess::Crashed: 402 setError(Crashed, "The Neovim process has crashed"); 403 break; 404 default: 405 // In practice we should be able to catch other types of 406 // errors from the QIODevice 407 qDebug() << "Neovim process error " << m_dev->errorString(); 408 } 409 } 410 411 /** Handle errors from QLocalSocket or QTcpSocket */ 412 void NeovimConnector::socketError() 413 { 414 setError(SocketError, m_dev->errorString()); 415 } 416 417 /** Handle errors in MsgpackIODevice */ 418 void NeovimConnector::msgpackError() 419 { 420 setError(MsgpackError, m_dev->errorString()); 421 } 422 423 /** 424 * Raise a fatal error for a Neovim timeout 425 * 426 * Sometimes Neovim takes too long to respond to some requests, or maybe 427 * the channel is stuck. In such cases it is preferable to raise and error, 428 * internally this is what discoverMetadata does if Neovim does not reply. 429 */ 430 void NeovimConnector::fatalTimeout() 431 { 432 setError(RuntimeMsgpackError, "Neovim is taking too long to respond"); 433 } 434 435 void NeovimConnector::setRequestHandler(MsgpackRequestHandler *h) 436 { 437 m_dev->setRequestHandler(h); 438 } 439 440 /** 441 * True if NeovimConnector::reconnect can be called to reconnect with Neovim. This 442 * is true unless you built the NeovimConnector ctor directly instead 443 * of using on of the static methods. 444 */ 445 bool NeovimConnector::canReconnect() 446 { 447 return m_ctype != OtherConnection; 448 } 449 450 /** @see NeovimConnector::NeovimConnectionType */ 451 NeovimConnector::NeovimConnectionType NeovimConnector::connectionType() 452 { 453 return m_ctype; 454 } 455 456 /** 457 * Create a new connection using the same parameters as the current one. 458 * 459 * This is the equivalent of creating a new object with spawn(), connectToHost(), 460 * or connectToSocket() 461 * 462 * If canReconnect() returns false, this function will return NULL. 463 */ 464 NeovimConnector* NeovimConnector::reconnect() 465 { 466 switch(m_ctype) { 467 case SpawnedConnection: 468 return NeovimConnector::spawn(m_spawnArgs, m_spawnExe); 469 case HostConnection: 470 return NeovimConnector::connectToHost(m_connHost, m_connPort); 471 case SocketConnection: 472 return NeovimConnector::connectToSocket(m_connSocket); 473 default: 474 return NULL; 475 } 476 // NOT-REACHED 477 return NULL; 478 } 479 480 /** The minimum API level supported by this instance */ 481 quint64 NeovimConnector::apiCompatibility() 482 { 483 return m_api_compat; 484 } 485 486 /** The maximum API level supported by this instance */ 487 quint64 NeovimConnector::apiLevel() 488 { 489 return m_api_supported; 490 } 491 492 /** Inspect Neovim metadata for ui_option support */ 493 bool NeovimConnector::hasUIOption(const QByteArray &option) 494 { 495 return m_uiOptions.contains(option); 496 } 497 498 /** 499 * \fn NeovimQt::NeovimConnector::error(NeovimError) 500 * 501 * This signal is emitted when an error occurs. Use NeovimConnector::errorString 502 * to get an error message. 503 */ 504 505 /** 506 * \fn NeovimQt::NeovimConnector::processExited(int exitStatus) 507 * 508 * If the Neovim process was started using NeovimQt::NeovimConnector::spawn this signal 509 * is emitted when the process exits. 510 */ 511 512 } // namespace NeovimQt 513 514 #include "moc_neovimconnector.cpp"